From 1a352df288af813dc205a858d63261406ef896b6 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:28:25 +0200 Subject: [PATCH 01/36] feat: add shared Studio-based codebook components and generator --- package-lock.json | 615 ++++++-- package.json | 14 +- .../codebook-export.component.html | 153 ++ .../codebook-export.component.scss | 44 + .../codebook-export.component.ts | 113 ++ .../codebook-export/ng-package.json | 6 + .../codebook-export/public-api.ts | 2 + .../codebook-docx-generator.ts | 949 ++++++++++++ .../codebook-generation-error.ts | 3 + .../codebook-generator/codebook-generator.ts | 89 ++ .../codebook-generator/manual-instruction.ts | 12 + .../codebook-generator/ng-package.json | 6 + .../codebook-generator/public-api.ts | 5 + .../codebook-generator/web-colors.ts | 1275 +++++++++++++++++ .../codebook-models/ng-package.json | 6 + .../codebook-models/public-api.ts | 155 ++ .../ngx-coding-components/ng-package.json | 8 +- projects/ngx-coding-components/package.json | 30 +- .../codebook-export.component.spec.ts | 60 + .../ngx-coding-components/tsconfig.spec.json | 3 + scripts/build-codebook-node.mjs | 18 + scripts/codebook-contract.test.mjs | 137 ++ scripts/fixtures/studio-codebook/README.md | 13 + scripts/fixtures/studio-codebook/document.xml | 1 + scripts/fixtures/studio-codebook/footer1.xml | 1 + .../fixtures/studio-codebook/numbering.xml | 1 + scripts/fixtures/studio-codebook/settings.xml | 1 + scripts/fixtures/studio-codebook/styles.xml | 1 + tsconfig.json | 3 + 29 files changed, 3596 insertions(+), 128 deletions(-) create mode 100644 projects/ngx-coding-components/codebook-export/codebook-export.component.html create mode 100644 projects/ngx-coding-components/codebook-export/codebook-export.component.scss create mode 100644 projects/ngx-coding-components/codebook-export/codebook-export.component.ts create mode 100644 projects/ngx-coding-components/codebook-export/ng-package.json create mode 100644 projects/ngx-coding-components/codebook-export/public-api.ts create mode 100644 projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts create mode 100644 projects/ngx-coding-components/codebook-generator/codebook-generation-error.ts create mode 100644 projects/ngx-coding-components/codebook-generator/codebook-generator.ts create mode 100644 projects/ngx-coding-components/codebook-generator/manual-instruction.ts create mode 100644 projects/ngx-coding-components/codebook-generator/ng-package.json create mode 100644 projects/ngx-coding-components/codebook-generator/public-api.ts create mode 100644 projects/ngx-coding-components/codebook-generator/web-colors.ts create mode 100644 projects/ngx-coding-components/codebook-models/ng-package.json create mode 100644 projects/ngx-coding-components/codebook-models/public-api.ts create mode 100644 projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts create mode 100644 scripts/build-codebook-node.mjs create mode 100644 scripts/codebook-contract.test.mjs create mode 100644 scripts/fixtures/studio-codebook/README.md create mode 100644 scripts/fixtures/studio-codebook/document.xml create mode 100644 scripts/fixtures/studio-codebook/footer1.xml create mode 100644 scripts/fixtures/studio-codebook/numbering.xml create mode 100644 scripts/fixtures/studio-codebook/settings.xml create mode 100644 scripts/fixtures/studio-codebook/styles.xml diff --git a/package-lock.json b/package-lock.json index 614f33e..e8eb020 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,13 +64,19 @@ "@tiptap/extension-underline": "^2.27.2", "@tiptap/pm": "^2.27.2", "@types/jasmine": "~4.3.0", + "@types/katex": "^0.16.8", "@types/node": "^20.4.4", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", + "buffer": "^6.0.3", + "cheerio": "^1.1.2", + "docx": "^8.5.0", + "esbuild": "0.28.1", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^18.0.0", "eslint-plugin-import": "^2.32.0", + "image-size": "^1.2.1", "iqb-dev-components": "^1.4.1", "jasmine-core": "~4.6.0", "karma": "~6.4.0", @@ -78,6 +84,7 @@ "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", + "mathml2omml": "^0.5.0", "ng-packagr": "^21.2.7", "ngx-build-plus": "^20.0.0", "ngx-tiptap": "^12.0.0", @@ -4604,9 +4611,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4760,9 +4764,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4780,9 +4781,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -4800,9 +4798,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4820,9 +4815,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4840,9 +4832,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4860,9 +4849,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4880,9 +4866,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5463,9 +5446,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5484,9 +5464,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5505,9 +5482,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5526,9 +5500,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5547,9 +5518,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5568,9 +5536,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5589,9 +5554,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5610,9 +5572,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5834,9 +5793,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5858,9 +5814,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5882,9 +5835,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5906,9 +5856,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -5930,9 +5877,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -5954,9 +5898,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6330,9 +6271,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6350,9 +6288,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6370,9 +6305,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6390,9 +6322,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6613,9 +6542,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6630,9 +6556,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6647,9 +6570,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6664,9 +6584,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6681,9 +6598,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6698,9 +6612,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6715,9 +6626,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6732,9 +6640,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6749,9 +6654,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6766,9 +6668,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -6783,9 +6682,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6800,9 +6696,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6817,9 +6710,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -7832,6 +7722,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/katex": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz", + "integrity": "sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", @@ -8938,6 +8835,27 @@ "dev": true, "license": "MIT" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -9126,6 +9044,31 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -9363,6 +9306,106 @@ "dev": true, "license": "MIT" }, + "node_modules/cheerio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", + "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.0.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.12.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -10359,6 +10402,42 @@ "node": ">=6.0.0" } }, + "node_modules/docx": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/docx/-/docx-8.5.0.tgz", + "integrity": "sha512-4SbcbedPXTciySXiSnNNLuJXpvxFe5nqivbiEHXyL8P/w0wx2uW7YXNjnYgjW0e2e6vy+L/tMISU/oAiXCl57Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^20.3.1", + "jszip": "^3.10.1", + "nanoid": "^5.0.4", + "xml": "^1.0.1", + "xml-js": "^1.6.8" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/docx/node_modules/nanoid": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, "node_modules/dom-serialize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", @@ -10487,6 +10566,33 @@ "node": ">= 0.8" } }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/encoding-sniffer/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/engine.io": { "version": "6.6.10", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.10.tgz", @@ -12502,6 +12608,27 @@ "postcss": "^8.1.0" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -12564,6 +12691,29 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "dev": true, + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, "node_modules/immutable": { "version": "5.1.9", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.9.tgz", @@ -13582,6 +13732,59 @@ ], "license": "MIT" }, + "node_modules/jszip": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.2.tgz", + "integrity": "sha512-3l+rb15IOWtUhU0H5MFqES/T6Kh7abYwjosBey/vD6hDt8zoEffkSC5Ws5SGtgVw3gBx2NEbhTeSW1+kWkpyTQ==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/karma": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", @@ -14229,6 +14432,16 @@ } } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -14726,6 +14939,13 @@ "node": ">= 18" } }, + "node_modules/mathml2omml": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/mathml2omml/-/mathml2omml-0.5.0.tgz", + "integrity": "sha512-4eLs37a+TH+CL/M5XZZrlc75SNRJNPiZzIaeSSvH0UFCRaCdSYRHWvrlA7MUbeQks/z4sVhfg+GlcZhVVUHzqg==", + "dev": true, + "license": "LGPL-3.0-or-later" + }, "node_modules/mdurl": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", @@ -16050,6 +16270,13 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -16139,6 +16366,85 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/parse5-parser-stream/node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/parse5-sax-parser": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-8.0.0.tgz", @@ -16934,6 +17240,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -17665,7 +17981,6 @@ "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", "dev": true, "license": "BlueOak-1.0.0", - "optional": true, "engines": { "node": ">=11.0.0" } @@ -17977,6 +18292,13 @@ "node": ">= 0.4" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -20351,6 +20673,43 @@ "node": ">=0.8.0" } }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -20566,6 +20925,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true, + "license": "MIT" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index d2ec08d..7e384f5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build:elements": "ng build --project coding-components -c elements", "serve:elements": "npx http-server dist/coding-components-elements -p 8081", "buildxx": "ng run coding-components:build", - "build_cc": "rm -rf dist/ngx-coding-components && ng build ngx-coding-components", + "build_cc": "rm -rf dist/ngx-coding-components && ng build ngx-coding-components && node scripts/build-codebook-node.mjs", "build": "npm run build_cc && npm run start", "build_sc": "rm -f dist/*.* && rm -rf dist/schemer && ng build --project schemer --output-hashing=none", "pack_sc": "node scripts/wrap_and_pack.js schemer $npm_package_config_schemer_version projects/schemer/src/html_wrapper/index.html", @@ -31,7 +31,8 @@ "test:schemer:watch": "ng test schemer", "test:schemer:coverage": "ng test schemer --watch=false --browsers=ChromeHeadless --code-coverage", "test:coverage": "npm run test:cc:coverage && npm run test:schemer:coverage", - "test:scheme": "node --test scripts/validate-coding-scheme.test.js" + "test:scheme": "node --test scripts/validate-coding-scheme.test.js", + "test:codebook": "node --test scripts/codebook-contract.test.mjs" }, "dependencies": { "@angular/animations": "^21.2.23", @@ -120,13 +121,18 @@ "@tiptap/extension-underline": "^2.27.2", "@tiptap/pm": "^2.27.2", "@types/jasmine": "~4.3.0", + "@types/katex": "^0.16.8", "@types/node": "^20.4.4", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", + "buffer": "^6.0.3", + "cheerio": "^1.1.2", + "docx": "^8.5.0", "eslint": "^8.57.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^18.0.0", "eslint-plugin-import": "^2.32.0", + "image-size": "^1.2.1", "iqb-dev-components": "^1.4.1", "jasmine-core": "~4.6.0", "karma": "~6.4.0", @@ -134,11 +140,13 @@ "karma-coverage": "~2.2.0", "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", + "mathml2omml": "^0.5.0", "ng-packagr": "^21.2.7", "ngx-build-plus": "^20.0.0", "ngx-tiptap": "^12.0.0", "ts-node": "^10.9.1", - "typescript": "5.9.3" + "typescript": "5.9.3", + "esbuild": "0.28.1" }, "overrides": { "@babel/core": "7.29.7", diff --git a/projects/ngx-coding-components/codebook-export/codebook-export.component.html b/projects/ngx-coding-components/codebook-export/codebook-export.component.html new file mode 100644 index 0000000..5b75947 --- /dev/null +++ b/projects/ngx-coding-components/codebook-export/codebook-export.component.html @@ -0,0 +1,153 @@ +
+

{{ 'codebook.workspace.export-coding-book' | translate }}

+ +
+
+
+ @if (selection.length === 0) {

{{ 'codebook.workspace.select-unit' | translate }}

} + @if (selection.length === 1) {

{{ 'codebook.workspace.unit-selected' | translate }}

} + @if (selection.length > 1) {

{{ selection.length }} {{ 'codebook.workspace.units-selected' | translate }}

} +
+ + {{ 'codebook.search-filter.filter-units' | translate }} + + + + + + + + + + + + + + {{ 'codebook.workspace.unit-key' | translate }} + {{ unit.key }} + + + {{ 'codebook.workspace.unit-name' | translate }} + {{ unit.unitName }} + + + {{ 'codebook.workspace.group' | translate }} + {{ unit.groupName }} + + + + +
+ {{ 'codebook.coding.selected-units' | translate: { count: selection.length } }} +
+
+
+
+
+

{{'codebook.coding.codebook-content' | translate}}

+
+ + {{'codebook.coding.has-only-vars-with-codes' | translate}} + + + {{'codebook.coding.has-only-manual-coding' | translate}} + + + {{'codebook.coding.has-closed-vars' | translate}} + + + {{'codebook.coding.has-derived-vars' | translate}} + +
+
+ +
+

{{'codebook.coding.codebook-formatting' | translate}}

+
+ + {{'codebook.coding.has-general-instructions' | translate}} + + + {{'codebook.coding.show-score' | translate}} + + + {{'codebook.coding.code-label-to-upper' | translate}} + + + {{'codebook.coding.hide-item-var-relation' | translate}} + +
+
+ +
+

{{ 'codebook.workspace.coding-missing-profiles' | translate }}

+ + — + @for (missingsProfile of missingsProfiles; track missingsProfile.id) { + + {{missingsProfile.label}} + + } + +
+ +
+

{{'codebook.coding.export-format' | translate}}

+ + JSON + DOCX + +
+ + + @if(workspaceChanges) { + {{'codebook.coding.error-save-changes' | translate}} + } +
+
+ +
+
+ + + + + + +
diff --git a/projects/ngx-coding-components/codebook-export/codebook-export.component.scss b/projects/ngx-coding-components/codebook-export/codebook-export.component.scss new file mode 100644 index 0000000..95a6ab1 --- /dev/null +++ b/projects/ngx-coding-components/codebook-export/codebook-export.component.scss @@ -0,0 +1,44 @@ +mat-select { + width: 100%; +} + +.settings { + margin-left: 20px; + min-width: 300px; +} + +.settings-group { + margin-bottom: 24px; + width: 100%; + + h4 { + margin-bottom: 8px; + border-bottom: 1px solid #eee; + padding-bottom: 4px; + color: #666; + } +} + + +.selection-summary { + margin-top: 12px; + padding: 8px; + background-color: #f5f5f5; + border-radius: 4px; + font-size: 0.9em; + color: #555; +} + +:host { display: block; } +.fx-column-start-stretch { display: flex; flex-direction: column; } +.fx-row-start-stretch, .fx-row-space-between-stretch { display: flex; align-items: stretch; gap: 16px; } +.fx-row-space-between-stretch { width: 100%; } +.fx-column-start-start { display: flex; flex-direction: column; align-items: flex-start; } +.fx-row-space-between-stretch > .fx-column-start-stretch { flex: 1; min-width: 320px; } +mat-table { width: 100%; max-height: 55vh; overflow: auto; } +mat-form-field { width: 100%; } +.mat-column-select { flex: 0 0 48px; } +@media (max-width: 760px) { + .fx-row-space-between-stretch { flex-direction: column; } + .settings { margin-left: 0; min-width: 0; } +} diff --git a/projects/ngx-coding-components/codebook-export/codebook-export.component.ts b/projects/ngx-coding-components/codebook-export/codebook-export.component.ts new file mode 100644 index 0000000..aae6de2 --- /dev/null +++ b/projects/ngx-coding-components/codebook-export/codebook-export.component.ts @@ -0,0 +1,113 @@ +import { + AfterViewInit, Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewChild +} from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { MatSort, MatSortModule } from '@angular/material/sort'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatSelectModule } from '@angular/material/select'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { TranslateModule } from '@ngx-translate/core'; +import type { + CodeBookContentSetting, CodebookExportConfig, MissingsProfile, UnitSelectionItem +} from '@iqb/ngx-coding-components/codebook-models'; + +export const DEFAULT_CODEBOOK_OPTIONS: CodeBookContentSetting = { + exportFormat: 'docx', + missingsProfile: '', + hasOnlyManualCoding: true, + hasGeneralInstructions: true, + hasDerivedVars: true, + hasOnlyVarsWithCodes: true, + hasClosedVars: true, + codeLabelToUpper: true, + showScore: true, + hideItemVarRelation: true +}; + +/** Studio's export form. Loading data, downloading and job handling belong to the host. */ +@Component({ + selector: 'ngx-codebook-export', + standalone: true, + imports: [FormsModule, MatTableModule, MatSortModule, MatCheckboxModule, MatDialogModule, + MatButtonModule, MatTooltipModule, MatSelectModule, MatRadioModule, MatFormFieldModule, + MatInputModule, TranslateModule], + templateUrl: './codebook-export.component.html', + styleUrls: ['./codebook-export.component.scss'] +}) +export class CodebookExportComponent implements OnChanges, AfterViewInit { + @Input() availableUnits: UnitSelectionItem[] = []; + @Input() missingsProfiles: MissingsProfile[] = []; + @Input() selectedUnitIds: number[] = []; + @Input() selectedMissingsProfileId = 0; + @Input() defaultContentOptions: Partial = {}; + @Input() loading = false; + @Input() busy = false; + @Input() workspaceChanges = false; + @Output() selectionChanged = new EventEmitter(); + @Output() exportRequested = new EventEmitter(); + @Output() cancel = new EventEmitter(); + @ViewChild(MatSort) sort!: MatSort; + + dataSource = new MatTableDataSource([]); + columns = ['select', 'key', 'name', 'group']; + selection: number[] = []; + filter = ''; + contentOptions = { ...DEFAULT_CODEBOOK_OPTIONS }; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['defaultContentOptions']) this.contentOptions = { ...DEFAULT_CODEBOOK_OPTIONS, ...this.defaultContentOptions }; + if (changes['availableUnits']) { + this.dataSource.data = this.availableUnits; + this.dataSource.filterPredicate = (unit, filter) => [unit.key, unit.unitName, unit.groupName || ''] + .some(value => value.toLowerCase().includes(filter)); + this.dataSource.sortingDataAccessor = (unit, column) => ({ key: unit.key, name: unit.unitName, group: unit.groupName || '' }[column] || ''); + } + if (changes['missingsProfiles'] && !this.missingsProfiles.some(profile => profile.id === this.selectedMissingsProfileId)) { + this.selectedMissingsProfileId = 0; + } + if (changes['selectedUnitIds']) this.selection = [...this.selectedUnitIds]; + const validIds = new Set(this.availableUnits.filter(unit => !unit.disabled).map(unit => unit.unitId)); + this.selection = this.selection.filter(id => validIds.has(id)); + } + + ngAfterViewInit(): void { this.dataSource.sort = this.sort; } + + get allSelected(): boolean { + const selectable = this.availableUnits.filter(unit => !unit.disabled); + return selectable.length > 0 && selectable.every(unit => this.selection.includes(unit.unitId)); + } + + get exportDisabled(): boolean { + return !this.selection.length || this.loading || this.busy || this.workspaceChanges; + } + + applyFilter(): void { this.dataSource.filter = this.filter.trim().toLowerCase(); } + + toggle(unit: UnitSelectionItem): void { + if (unit.disabled || this.busy) return; + this.selection = this.selection.includes(unit.unitId) ? this.selection.filter(id => id !== unit.unitId) : [...this.selection, unit.unitId]; + this.selectionChanged.emit([...this.selection]); + } + + toggleAll(): void { + if (this.busy) return; + this.selection = this.allSelected ? [] : this.availableUnits.filter(unit => !unit.disabled).map(unit => unit.unitId); + this.selectionChanged.emit([...this.selection]); + } + + exportCodingBook(): void { + if (this.exportDisabled) return; + const profile = this.missingsProfiles.find(item => item.id === this.selectedMissingsProfileId); + this.exportRequested.emit({ + selectedUnits: [...this.selection], + missingsProfileId: this.selectedMissingsProfileId, + contentOptions: { ...this.contentOptions, missingsProfile: profile?.label || '' } + }); + } +} diff --git a/projects/ngx-coding-components/codebook-export/ng-package.json b/projects/ngx-coding-components/codebook-export/ng-package.json new file mode 100644 index 0000000..ed27894 --- /dev/null +++ b/projects/ngx-coding-components/codebook-export/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/projects/ngx-coding-components/codebook-export/public-api.ts b/projects/ngx-coding-components/codebook-export/public-api.ts new file mode 100644 index 0000000..67bb635 --- /dev/null +++ b/projects/ngx-coding-components/codebook-export/public-api.ts @@ -0,0 +1,2 @@ +export * from './codebook-export.component'; +export type * from '@iqb/ngx-coding-components/codebook-models'; diff --git a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts new file mode 100644 index 0000000..9e570f7 --- /dev/null +++ b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts @@ -0,0 +1,949 @@ +import { + AlignmentType, + Document, + HeadingLevel, + ImageRun, + NumberFormat, + Packer, + Paragraph, + Table, + TableCell, + TableRow, + TextRun, + Footer, + WidthType, + PageNumber, + ITableCellBorders, + ImportedXmlComponent, + ParagraphChild +} from 'docx'; + +import { + CodeBookContentSetting, + CodebookUnitDto, + BookVariable, + ItemMetadata +} from '@iqb/ngx-coding-components/codebook-models'; +import * as cheerio from 'cheerio'; +import { Buffer } from 'buffer'; +import type { Element, AnyNode } from 'domhandler'; +import { BasicAcceptedElems } from 'cheerio'; + +import { imageSize } from 'image-size'; + +// katex is CommonJS with no statically detectable named exports: under ESM `import * as katex` +// yields only `{ default }`, and every `katex.x` is undefined. The default import works in both. +import katex from 'katex'; +import { mml2omml } from 'mathml2omml'; +import { WebColors } from './web-colors'; +// Type-only, like every deep import into a package: were it ever emitted as a runtime require, +// an `exports` map in that package would kill the boot -- see the swagger import in +// setting.controller.ts. +type FileChild = Paragraph | Table; + +export class CodebookDocxGenerator { + private static ommlCache: Record = {}; + + static async generateDocx( + codingBookUnits: CodebookUnitDto[], + contentSetting: CodeBookContentSetting + ): Promise { + const units: FileChild[] = []; + const missingDefinitions = new Map(); + codingBookUnits.forEach(unit => { + unit.missings.forEach(missing => missingDefinitions.set(JSON.stringify(missing), missing)); + if (unit.variables.length) { + units.push(...this.createDocXForUnit(unit.items || [], unit.variables, contentSetting, this.getUnitHeader(unit))); + } + }); + const missings = this.getMissings({ + key: '', name: '', variables: [], missings: [...missingDefinitions.values()] + }); + return Packer.toBlob(this.setDocXDocument(units, missings)); + } + + private static getUnitHeader(variableCoding: CodebookUnitDto): Paragraph { + return new Paragraph({ + border: { + bottom: { + color: '#000000', + style: 'single', + size: 10 + }, + top: { + color: '#000000', + style: 'single', + size: 10 + } + }, + spacing: { + after: 200 + }, + text: `${variableCoding.key} ${variableCoding.name}`, + heading: HeadingLevel.HEADING_1, + alignment: AlignmentType.CENTER + }); + } + + private static getMissings(variableCoding: CodebookUnitDto): Paragraph[] { + const missings: Paragraph[] = []; + try { + variableCoding.missings.forEach(missing => { + if (missing.code !== undefined && missing.code !== null && `${missing.code}` !== '' && missing.label && missing.description) { + missings.push( + new Paragraph({ + children: [ + new TextRun({ + text: `${missing.code} ${missing.label}`, + bold: true + }) + ], + spacing: { + after: 20 + } + }) + ); + missings.push( + new Paragraph({ + text: `${missing.description}`, + spacing: { + after: 100 + } + }) + ); + } else { + missings.push( + new Paragraph({ + text: 'kein valides Missing ', + spacing: { + after: 200 + } + }) + ); + } + }); + } catch { + missings.push( + new Paragraph({ + text: 'kein validen Missings gefunden', + spacing: { + after: 200 + } + }) + ); + } + return missings; + } + + private static get TableBoarders(): ITableCellBorders { + return { + top: { + size: 1, + color: '#000000', + style: 'single' + }, + bottom: { + size: 1, + color: '#000000', + style: 'single' + }, + left: { + size: 1, + color: '#000000', + style: 'single' + }, + right: { + size: 1, + color: '#000000', + style: 'single' + } + }; + } + + private static getCodeRows( + variable: BookVariable, + contentSetting: CodeBookContentSetting + ): TableRow[] { + return variable.codes.map( + code => new TableRow({ + cantSplit: true, + children: [ + CodebookDocxGenerator.createCodeCell( + CodebookDocxGenerator.createCellChildren(code.id), + CodebookDocxGenerator.getColumnWidths(contentSetting)[0] + ), + CodebookDocxGenerator.createCodeCell( + CodebookDocxGenerator.createCellChildren(code.label), + CodebookDocxGenerator.getColumnWidths(contentSetting)[1] + ), + ...(contentSetting.showScore ? + [ + CodebookDocxGenerator.createCodeCell( + CodebookDocxGenerator.createCellChildren(code.score ?? ''), + CodebookDocxGenerator.getColumnWidths(contentSetting)[2] + ) + ] : + []), + CodebookDocxGenerator.createCodeCell( + [...CodebookDocxGenerator.htmlToDocx(code.description, contentSetting)], + CodebookDocxGenerator.getColumnWidths(contentSetting)[ + CodebookDocxGenerator.getColumnWidths(contentSetting).length - 1 + ] + ) + ] + }) + ); + } + + private static createCellChildren(value: string): Paragraph[] { + return [ + new Paragraph({ + text: value, + spacing: { + before: 100, + after: 100 + }, + indent: { start: 100, end: 100 } + }) + ]; + } + + private static createCodeCell( + children: Paragraph[], + width: number + ): TableCell { + return new TableCell({ + borders: CodebookDocxGenerator.TableBoarders, + children: children, + // Need for Word, but not for Writer + width: { + size: width, + type: WidthType.PERCENTAGE + } + }); + } + + private static setDocXDocument( + units: FileChild[], + missings: Paragraph[] + ): Document { + return new Document({ + background: { + color: '#FFFFFF' + }, + sections: [ + { + properties: { + page: { + pageNumbers: { + start: 1, + formatType: NumberFormat.DECIMAL + } + } + }, + footers: { + default: new Footer({ + children: [ + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [ + new TextRun('IQB Codebook '), + new TextRun(new Date().toLocaleDateString()) + ] + }), + new Paragraph({ + alignment: AlignmentType.CENTER, + children: [ + new TextRun({ + color: '000000', + children: [' Seite ', PageNumber.CURRENT] + }), + new TextRun({ + color: '000000', + children: [' von ', PageNumber.TOTAL_PAGES] + }) + ] + }) + ] + }) + }, + children: [...missings, ...units] + } + ] + }); + } + + private static getVariableHeader(variable: BookVariable): Paragraph { + return new Paragraph({ + text: `${variable.id} ${variable.label}`, + heading: HeadingLevel.HEADING_2, + alignment: AlignmentType.LEFT, + spacing: { + before: 200, + after: 200 + } + }); + } + + private static getVariableItems( + variable: BookVariable, + varItems: ItemMetadata[] + ): Paragraph[] { + const filteredVarItems = varItems.filter( + item => item.variableId === variable.id + ); + let itemString = ''; + filteredVarItems.forEach(item => { + itemString += `${item.id} `; + }); + if (filteredVarItems.length === 0) { + return []; + } + + return [new Paragraph({ + text: `Item(s): ${itemString}`, + heading: HeadingLevel.HEADING_3, + alignment: AlignmentType.LEFT, + spacing: { + before: 200, + after: 200 + } + })]; + } + + private static getGeneralInstructions( + contentSetting: CodeBookContentSetting, + codeBookVariable: BookVariable + ): Paragraph[] { + return contentSetting.hasGeneralInstructions ? + CodebookDocxGenerator.htmlToDocx( + codeBookVariable.generalInstruction, + contentSetting + ) : + []; + } + + private static getCodeTable( + codeBookVariable: BookVariable, + contentSetting: CodeBookContentSetting + ): Table { + return new Table({ + rows: CodebookDocxGenerator.getCodeRows(codeBookVariable, contentSetting), + width: { + size: 100, + type: WidthType.PERCENTAGE + }, + columnWidths: CodebookDocxGenerator.getColumnWidths(contentSetting) // Need for Writer, but not for Word + }); + } + + private static getColumnWidths( + contentSetting: CodeBookContentSetting + ): number[] { + return contentSetting.showScore ? [8, 24, 8, 60] : [8, 24, 68]; + } + + private static getVariables( + codeBookVariable: BookVariable[], + contentSetting: CodeBookContentSetting, + varItems: ItemMetadata[] + ): FileChild[] { + const variables: FileChild[] = []; + codeBookVariable.forEach(variable => { + variables.push( + ...[ + CodebookDocxGenerator.getVariableHeader(variable), + ...(contentSetting.hideItemVarRelation ? [] : CodebookDocxGenerator.getVariableItems(variable, varItems)), + ...CodebookDocxGenerator.getGeneralInstructions(contentSetting, variable), + CodebookDocxGenerator.getCodeTable(variable, contentSetting) + ] + ); + }); + return variables; + } + + private static createDocXForUnit( + varItems: ItemMetadata[], + codeBookVariable: BookVariable[], + contentSetting: CodeBookContentSetting, + unitHeader: Paragraph + ): FileChild[] { + return [ + unitHeader, + ...CodebookDocxGenerator.getVariables(codeBookVariable, contentSetting, varItems), + new Paragraph({ + text: '', + spacing: { + before: 100, + after: 100 + } + }) + ]; + } + + private static getChildren( + cheerioAPI: cheerio.CheerioAPI, + elem: BasicAcceptedElems + ): AnyNode[] { + return cheerioAPI(elem).toArray(); + } + + private static processInlineElements( + nodes: AnyNode[], + children: ParagraphChild[], + colorParsed: string, + backgroundColor: string, + size: string + ): void { + nodes.forEach(node => { + if (node.type === 'text') { + if ('data' in node && node.data && node.data.trim()) { + children.push( + new TextRun({ + text: node.data.replace(/\s+/g, ' '), + color: colorParsed, + shading: { + fill: backgroundColor + }, + size: CodebookDocxGenerator.getFontSize(size) + }) + ); + } + } else if (node.type === 'tag') { + const element = node as Element; + const tagName = element.name.toLowerCase(); + + if ( + tagName === 'span' && + element.attribs?.['class']?.includes('iqb-math-formula') + ) { + const rawLatex = element.attribs?.['data-latex'] || ''; + const latex = CodebookDocxGenerator.decodeLatex(rawLatex).trim(); + if (latex) { + const ommlComponent = CodebookDocxGenerator.latexToOmml(latex); + if (ommlComponent) { + children.push(ommlComponent); + } else { + children.push( + new TextRun({ + text: latex, + color: colorParsed, + shading: { + fill: backgroundColor + }, + size: CodebookDocxGenerator.getFontSize(size) + }) + ); + } + } + return; + } + + if (tagName === 'strong' || tagName === 'b') { + if (element.children) { + element.children.forEach(child => { + if (child.type === 'text' && child.data) { + children.push( + new TextRun({ + text: child.data.replace(/\s+/g, ' '), + bold: true, + color: colorParsed, + shading: { + fill: backgroundColor + }, + size: CodebookDocxGenerator.getFontSize(size) + }) + ); + } + }); + } + } else if (tagName === 'em' || tagName === 'i') { + if (element.children) { + element.children.forEach(child => { + if (child.type === 'text' && child.data) { + children.push( + new TextRun({ + text: child.data.replace(/\s+/g, ' '), + italics: true, + color: colorParsed, + shading: { + fill: backgroundColor + }, + size: CodebookDocxGenerator.getFontSize(size) + }) + ); + } + }); + } + } else if (tagName === 'u') { + if (element.children) { + element.children.forEach(child => { + if (child.type === 'text' && child.data) { + children.push( + new TextRun({ + text: child.data.replace(/\s+/g, ' '), + underline: {}, + color: colorParsed, + shading: { + fill: backgroundColor + }, + size: CodebookDocxGenerator.getFontSize(size) + }) + ); + } + }); + } + } else if (tagName === 's') { + if (element.children) { + element.children.forEach(child => { + if (child.type === 'text' && child.data) { + children.push( + new TextRun({ + text: child.data.replace(/\s+/g, ' '), + strike: true, + color: colorParsed, + shading: { + fill: backgroundColor + }, + size: CodebookDocxGenerator.getFontSize(size) + }) + ); + } + }); + } + } else if (tagName === 'sub') { + if (element.children) { + element.children.forEach(child => { + if (child.type === 'text' && child.data) { + children.push( + new TextRun({ + text: child.data.replace(/\s+/g, ' '), + subScript: true, + color: colorParsed, + shading: { + fill: backgroundColor + }, + size: CodebookDocxGenerator.getFontSize(size) + }) + ); + } + }); + } + } else if (tagName === 'sup') { + if (element.children) { + element.children.forEach(child => { + if (child.type === 'text' && child.data) { + children.push( + new TextRun({ + text: child.data.replace(/\s+/g, ' '), + superScript: true, + color: colorParsed, + shading: { + fill: backgroundColor + }, + size: CodebookDocxGenerator.getFontSize(size) + }) + ); + } + }); + } + } else if (tagName === 'br') { + children.push( + new TextRun({ + break: 1, + color: colorParsed, + shading: { + fill: backgroundColor + }, + size: CodebookDocxGenerator.getFontSize(size) + }) + ); + } else if (element.children && element.children.length > 0) { + CodebookDocxGenerator.processInlineElements( + element.children, + children, + colorParsed, + backgroundColor, + size + ); + } + } + }); + } + + private static getImageSize(imageBuffer: Buffer): { width: number; height: number } { + const size = imageSize(imageBuffer as unknown as Uint8Array); + if (!size.width || !size.height) throw new Error('Ungültige Bildgröße'); + return { width: size.width, height: size.height }; + } + + private static getTransformation( + actualSize: { width: number; height: number }, + max: number + ): { width: number; height: number } { + const transformedSize: { width: number; height: number } = { + width: actualSize.width, + height: actualSize.height + }; + const maxWidth = max; + const maxHeight = max; + if (actualSize.width > maxWidth || actualSize.height > maxHeight) { + const ratio = Math.min( + maxWidth / actualSize.width, + maxHeight / actualSize.height + ); + transformedSize.width = actualSize.width * ratio; + transformedSize.height = actualSize.height * ratio; + } + return transformedSize; + } + + private static createImagePragraph( + elem: Element, + contentSetting: CodeBookContentSetting + ): Paragraph { + const imageBuffer = Buffer.from( + elem.attribs['src'].substring(elem.attribs['src'].indexOf(',') + 1), + 'base64' + ); + const size = CodebookDocxGenerator.getImageSize(imageBuffer); + return new Paragraph({ + spacing: { + before: 100, + after: 100 + }, + indent: { + start: 100, + end: 100 + }, + children: [ + new ImageRun({ + data: imageBuffer, + transformation: CodebookDocxGenerator.getTransformation( + size, + contentSetting.showScore ? 334 : 382 + ) + }) + ] + }); + } + + private static getBackgroundColor( + cheerioAPI: cheerio.CheerioAPI, + elem: Element + ): string { + const mark = cheerioAPI(elem).find('mark'); + return cheerioAPI(mark).css('background-color') || '#FFFFFF'; + } + + private static getColor( + cheerioAPI: cheerio.CheerioAPI, + tag: cheerio.Cheerio + ): string { + const color = cheerioAPI(tag).css('color'); + // const name = elem.name; + let colorParsed: string = '#000000'; + if (color) { + if (color.startsWith('#')) { + colorParsed = color; + } else if (color.startsWith('rgb')) { + const rgbString = CodebookDocxGenerator.parseCssRgbString(color); + if (rgbString) { + colorParsed = CodebookDocxGenerator.toHex( + rgbString[0], + rgbString[1], + rgbString[2] + ); + } + } else if (WebColors.getHexFromWebColor(color.toLowerCase())) { + colorParsed = `#${WebColors.getHexFromWebColor(color.toLowerCase())}`; + } + } + return colorParsed; + } + + private static getSize( + cheerioAPI: cheerio.CheerioAPI, + tag: cheerio.Cheerio + ): string { + return cheerioAPI(tag).css('font-size') || '20pt'; + } + + private static getTextAlignment( + cheerioAPI: cheerio.CheerioAPI, + elem: Element + ): string { + return cheerioAPI(elem).css('text-align') || 'left'; + } + + private static stripFromLeadingEmptyParagraph(html: string): string { + return html.replace(/^

<\/p>/, ''); + } + + private static convertLineBreaksToHTMLBreaks(html: string): string { + return html.replace(/\n/g, '
'); + } + + private static prepareHtml(html: string): string { + return CodebookDocxGenerator.convertLineBreaksToHTMLBreaks( + CodebookDocxGenerator.stripFromLeadingEmptyParagraph(html) + ); + } + + private static decodeLatex(latex: string): string { + if (!latex) return ''; + try { + return decodeURIComponent(latex); + } catch { + return latex; + } + } + + private static latexToOmml(latex: string): ImportedXmlComponent | null { + const trimmed = latex.trim(); + if (!trimmed) return null; + try { + if (!CodebookDocxGenerator.ommlCache[trimmed]) { + const mathml = katex.renderToString(trimmed, { + output: 'mathml', + throwOnError: false, + strict: 'ignore' + }); + CodebookDocxGenerator.ommlCache[trimmed] = mml2omml(mathml); + } + const omml = CodebookDocxGenerator.ommlCache[trimmed]; + return CodebookDocxGenerator.unwrapImportedXmlRoot( + ImportedXmlComponent.fromXmlString( + CodebookDocxGenerator.sanitizeOmmlXml(omml) + ) + ); + } catch { + return null; + } + } + + private static unwrapImportedXmlRoot( + component: ImportedXmlComponent + ): ImportedXmlComponent { + const importedComponent = component as ImportedXmlComponent & { + root?: Array; + rootKey?: string; + }; + if (importedComponent.rootKey !== undefined) { + return component; + } + + const firstChild = importedComponent.root?.[0]; + return typeof firstChild === 'object' && firstChild !== null ? + firstChild as ImportedXmlComponent : + component; + } + + private static isMathComponent( + child: ParagraphChild + ): child is ImportedXmlComponent { + if (typeof child !== 'object' || child === null) { + return false; + } + const importedChild = child as ImportedXmlComponent & { + rootKey?: string; + }; + return importedChild.rootKey === 'm:oMath' || + importedChild.rootKey === 'm:oMathPara'; + } + + private static createLeftAlignedMathParagraph( + mathChildren: ImportedXmlComponent[] + ): ImportedXmlComponent { + const mathParagraph = new ImportedXmlComponent('m:oMathPara'); + const mathParagraphProperties = new ImportedXmlComponent('m:oMathParaPr'); + mathParagraphProperties.push( + new ImportedXmlComponent('m:jc', { 'm:val': 'left' }) + ); + mathParagraph.push(mathParagraphProperties); + mathChildren.forEach(mathChild => mathParagraph.push(mathChild)); + return mathParagraph; + } + + private static normalizeParagraphChildren( + children: ParagraphChild[] + ): ParagraphChild[] { + if (!children.length) { + return children; + } + + const mathChildren = children.filter( + CodebookDocxGenerator.isMathComponent + ) as ImportedXmlComponent[]; + if (mathChildren.length === children.length) { + return [CodebookDocxGenerator.createLeftAlignedMathParagraph(mathChildren)]; + } + + return children; + } + + private static sanitizeOmmlXml(omml: string): string { + if (!omml) return omml; + return omml.replace( + /(]*>)([\s\S]*?)(<\/m:t>)/g, + (_, prefix: string, rawText: string, suffix: string) => `${prefix}${CodebookDocxGenerator.escapeXmlText(rawText)}${suffix}` + ); + } + + private static escapeXmlText(text: string): string { + return text + .replace( + /&(?!(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);)/g, + '&' + ) + .replace(//g, '>'); + } + + private static normalizeMathTokens(html: string): string { + return html.replace( + /\[\[iqb-math:([\s\S]*?)]]/g, + (_, encodedFormula: string) => { + const latex = this.decodeLatex(encodedFormula); + const escapedLatex = latex.replace(/"/g, '"'); + return ``; + } + ); + } + + private static isListParagraph(elem: Element): boolean { + return !!elem.parent && (elem.parent as Element).name === 'li'; + } + + private static htmlToDocx( + html: string, + contentSetting: CodeBookContentSetting + ) { + const normalizedHtml = CodebookDocxGenerator.normalizeMathTokens(html); + const blockHtml = /<(p|h[1-4]|img|ul|ol)\b/i.test(normalizedHtml) ? normalizedHtml : `

${normalizedHtml}

`; + const cheerioAPI = cheerio.load( + CodebookDocxGenerator.prepareHtml(blockHtml), + null, + false + ); + const elements: Paragraph[] = []; + cheerioAPI('p,h1,h2,h3,h4,img').each((i, elem) => { + try { + const span = cheerioAPI(elem).find('span'); + if (elem.name === 'img') { + elements.push(CodebookDocxGenerator.createImagePragraph(elem, contentSetting)); + } else { + elements.push( + CodebookDocxGenerator.createParagraph( + cheerioAPI, + elem.children, + CodebookDocxGenerator.getTextAlignment(cheerioAPI, elem), + CodebookDocxGenerator.getColor(cheerioAPI, span), + CodebookDocxGenerator.getBackgroundColor(cheerioAPI, elem), + CodebookDocxGenerator.getSize(cheerioAPI, span), + CodebookDocxGenerator.isListParagraph(elem) + ) + ); + } + } catch { + elements.push( + new Paragraph({ + text: 'HTML konnte nicht verarbeitet werden.' + }) + ); + } + }); + return elements.filter(e => e !== undefined && e !== null); + } + + private static getFontSize(size: string): number { + const sizeTypes = [ + 'xx-small', + 'x-small', + 'small', + 'medium', + 'large', + 'x-large', + 'xx-large' + ]; + return sizeTypes.includes(size) ? 20 : parseInt(size, 10); + } + + private static getAlignment( + textAlignment: string + ): typeof AlignmentType[keyof typeof AlignmentType] { + switch (textAlignment) { + case 'center': + return AlignmentType.CENTER; + case 'right': + return AlignmentType.RIGHT; + case 'justify': + return AlignmentType.JUSTIFIED; + default: + return AlignmentType.LEFT; + } + } + + private static createParagraph( + cheerioAPI: cheerio.CheerioAPI, + elem: BasicAcceptedElems, + textAlignment: string, + colorParsed: string, + backgroundColor: string, + size: string, + isListParagraph: boolean + ): Paragraph { + const children: ParagraphChild[] = []; + const childNodes = CodebookDocxGenerator.getChildren(cheerioAPI, elem); + CodebookDocxGenerator.processInlineElements( + childNodes, + children, + colorParsed, + backgroundColor, + size + ); + const normalizedChildren = CodebookDocxGenerator.normalizeParagraphChildren( + children + ); + return new Paragraph({ + alignment: CodebookDocxGenerator.getAlignment(textAlignment), + spacing: { + before: 100, + after: 100 + }, + indent: !isListParagraph ? { start: 100, end: 100 } : undefined, + bullet: isListParagraph ? { level: 0 } : undefined, + children: normalizedChildren + }); + } + + private static parseCssRgbString(input: string) { + const parts = input + ?.replace(/rgba?\(([^)]+)\)/, '$1') + .split(/[,\s/]+/) + .filter(Boolean); + if (parts?.length < 3) { + return; + } + + const parseValue = (value: string, max: number) => { + // eslint-disable-next-line no-param-reassign + value = value.trim(); + + if (value.endsWith('%')) { + return Math.min((Number.parseFloat(value) * max) / 100, max); + } + return Math.min(Number.parseFloat(value), max); + }; + const red = parseValue(parts[0], 255); + const green = parseValue(parts[1], 255); + const blue = parseValue(parts[2], 255); + + // eslint-disable-next-line consistent-return + return [red, green, blue]; + } + + // Convert RGB color to HEX https://github.com/sindresorhus/rgb-hex + // eslint-disable-next-line no-bitwise + private static toHex = (red: number, green: number, blue: number) => (blue | (green << 8) | (red << 16) | (1 << 24)).toString(16).slice(1); +} diff --git a/projects/ngx-coding-components/codebook-generator/codebook-generation-error.ts b/projects/ngx-coding-components/codebook-generator/codebook-generation-error.ts new file mode 100644 index 0000000..d6f9f32 --- /dev/null +++ b/projects/ngx-coding-components/codebook-generator/codebook-generation-error.ts @@ -0,0 +1,3 @@ +export class CodebookGenerationError extends Error { + override name = 'CodebookGenerationError'; +} diff --git a/projects/ngx-coding-components/codebook-generator/codebook-generator.ts b/projects/ngx-coding-components/codebook-generator/codebook-generator.ts new file mode 100644 index 0000000..9005e3e --- /dev/null +++ b/projects/ngx-coding-components/codebook-generator/codebook-generator.ts @@ -0,0 +1,89 @@ +import { ToTextFactory } from '@iqb/responses'; +import { CodingScheme, CodeData, VariableCodingData } from '@iqbspecs/coding-scheme'; +import type { + BookVariable, CodeBookContentSetting, CodebookUnitDto, CodeInfo, Missing, UnitPropertiesForCodebook +} from '@iqb/ngx-coding-components/codebook-models'; +import { CodebookGenerationError } from './codebook-generation-error'; +import { hasCodebookManualInstruction } from './manual-instruction'; + +export class CodebookGenerator { + static getCodebookData(units: UnitPropertiesForCodebook[], options: CodeBookContentSetting, missings: Missing[]): CodebookUnitDto[] { + return [...units].sort((a, b) => (a.key < b.key ? -1 : Number(a.key > b.key))).map(unit => { + let variables: VariableCodingData[] = []; + if (unit.scheme) { + try { + const data = JSON.parse(unit.scheme); + if (!Array.isArray(data.variableCodings) || data.variableCodings.some((variable: VariableCodingData) => !variable || typeof variable.id !== 'string' || + (variable.codes !== undefined && (!Array.isArray(variable.codes) || variable.codes.some(code => !code || typeof code !== 'object'))))) { + throw new Error('Ungültige variableCodings'); + } + variables = new CodingScheme(unit.scheme).variableCodings; + } catch { + throw new CodebookGenerationError(`Ungültiges Kodierschema für Aufgabe ${unit.key}.`); + } + } + const bookVariables = variables.map(variable => this.getVariable(variable, options)) + .filter((variable): variable is BookVariable => variable !== null) + .sort((a, b) => (a.id < b.id ? -1 : Number(a.id > b.id))); + return { + key: unit.key, name: unit.name, variables: bookVariables, missings, items: unit.metadata?.items || [] + }; + }); + } + + static async generateCodebook(units: UnitPropertiesForCodebook[], options: CodeBookContentSetting, missings: Missing[]): Promise { + const data = this.getCodebookData(units, options, missings); + if (options.exportFormat === 'docx') { + const { CodebookDocxGenerator } = await import('./codebook-docx-generator'); + return CodebookDocxGenerator.generateDocx(data, options); + } + return new Blob([JSON.stringify(data.map(({ + key, name, variables, missings: unitMissings + }) => ({ + key, name, variables, missings: unitMissings + })))], { type: 'application/json' }); + } + + private static isClosed(code: CodeData): boolean { + return code.type === 'RESIDUAL_AUTO' || code.type === 'INTENDED_INCOMPLETE'; + } + + private static isManual(code: CodeData): boolean { + return !this.isClosed(code) && hasCodebookManualInstruction(code.manualInstruction); + } + + private static getVariable(variable: VariableCodingData, options: CodeBookContentSetting): BookVariable | null { + if (variable.sourceType === 'BASE_NO_VALUE' || (variable.sourceType !== 'BASE' && !options.hasDerivedVars)) return null; + const codes = (variable.codes || []).filter(code => code.id !== undefined && code.id !== null); + const manual = codes.some(code => this.isManual(code)); + const closed = codes.some(code => this.isClosed(code)); + if ((options.hasOnlyManualCoding || options.hasClosedVars) && + !((options.hasOnlyManualCoding && manual) || (options.hasClosedVars && closed))) return null; + const exportedCodes = options.hasOnlyManualCoding && !options.hasClosedVars ? codes.filter(code => this.isManual(code)) : codes; + if (options.hasOnlyVarsWithCodes && !exportedCodes.length) return null; + return { + id: variable.alias || variable.id, + label: variable.label || '', + sourceType: variable.sourceType, + generalInstruction: options.hasGeneralInstructions ? variable.manualInstruction || '' : '', + codes: exportedCodes.map(code => this.getCode(code, options)) + }; + } + + private static getCode(code: CodeData, options: CodeBookContentSetting): CodeInfo { + const fallback: CodeInfo = { id: `${code.id}`, label: '', description: '

Kodierschema mit Schemer Version ab 1.5 erzeugen!

' }; + if (options.showScore) fallback.score = ''; + if (Object.prototype.hasOwnProperty.call(code, 'rules')) return fallback; + try { + const text = ToTextFactory.codeAsText(code, 'SIMPLE'); + const rules = options.hasOnlyManualCoding && !options.hasClosedVars ? '' : + (text.ruleSetDescriptions || []).filter(rule => rule !== 'Keine Regeln definiert.' || !code.manualInstruction) + .map(rule => `

${rule}

`).join(''); + const result: CodeInfo = { id: `${code.id}`, label: options.codeLabelToUpper ? text.label.toUpperCase() : text.label, description: `${rules}${code.manualInstruction || ''}` }; + if (options.showScore) result.score = `${text.score}`; + return result; + } catch { + return fallback; + } + } +} diff --git a/projects/ngx-coding-components/codebook-generator/manual-instruction.ts b/projects/ngx-coding-components/codebook-generator/manual-instruction.ts new file mode 100644 index 0000000..0a42602 --- /dev/null +++ b/projects/ngx-coding-components/codebook-generator/manual-instruction.ts @@ -0,0 +1,12 @@ +import { load } from 'cheerio'; + +/** Rich-text instructions may consist entirely of an image or a formula. */ +export function hasCodebookManualInstruction(instruction?: string | null): boolean { + if (!instruction) return false; + const $ = load(instruction); + $('script,style').remove(); + const text = $.root().text().replace(/[\s\u200b-\u200d\ufeff]/g, ''); + if (text) return true; + return $('img').toArray().some(element => /^data:image\/(png|jpeg|jpg|gif|bmp);base64,\S+$/i.test($(element).attr('src') || '')) || + $('span.iqb-math-formula').toArray().some(element => !!$(element).attr('data-latex')?.trim()); +} diff --git a/projects/ngx-coding-components/codebook-generator/ng-package.json b/projects/ngx-coding-components/codebook-generator/ng-package.json new file mode 100644 index 0000000..ed27894 --- /dev/null +++ b/projects/ngx-coding-components/codebook-generator/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/projects/ngx-coding-components/codebook-generator/public-api.ts b/projects/ngx-coding-components/codebook-generator/public-api.ts new file mode 100644 index 0000000..017d25a --- /dev/null +++ b/projects/ngx-coding-components/codebook-generator/public-api.ts @@ -0,0 +1,5 @@ +export * from './codebook-generator'; +export * from './codebook-docx-generator'; +export * from './manual-instruction'; +export type * from '@iqb/ngx-coding-components/codebook-models'; +export * from './codebook-generation-error'; diff --git a/projects/ngx-coding-components/codebook-generator/web-colors.ts b/projects/ngx-coding-components/codebook-generator/web-colors.ts new file mode 100644 index 0000000..df9d391 --- /dev/null +++ b/projects/ngx-coding-components/codebook-generator/web-colors.ts @@ -0,0 +1,1275 @@ +/** + * The CSS colour names and their hex values, for turning a colour a user picked by name into + * something a generated document can paint with. + */ +export class WebColors { + /** + * The hex value for a CSS colour name, or an empty string when the name is unknown. The name has + * to be given in lower case -- the table is compared against it as it comes in. + */ + static getHexFromWebColor(colorName: string) { + const foundColor = WebColors.colors + .find(color => color.name.toLowerCase() === colorName); + return foundColor ? foundColor.hex : ''; + } + + /** The table itself: name, hex and rgb for every CSS colour. */ + static colors = [ + { + name: 'Lavender', + hex: 'E6E6FA', + rgb: { + r: 230, + g: 230, + b: 250 + } + }, + { + name: 'Thistle', + hex: 'D8BFD8', + rgb: { r: 216, g: 191, b: 216 } + }, + { + name: 'Plum', + hex: 'DDA0DD', + rgb: { + r: 221, + g: 160, + b: 221 + } + }, + { + name: 'Violet', + hex: 'EE82EE', + rgb: { + r: 238, + g: 130, + b: 238 + } + }, + { + name: 'Orchid', + hex: 'DA70D6', + rgb: { + r: 218, + g: 112, + b: 214 + } + }, + { + name: 'Fuchsia', + hex: 'FF00FF', + rgb: { + r: 255, + g: 0, + b: 255 + } + }, + { + name: 'Magenta', + hex: 'FF00FF', + rgb: { + r: 255, + g: 0, + b: 255 + } + }, + { + name: 'MediumOrchid', + hex: 'BA55D3', + rgb: { + r: 186, + g: 85, + b: 211 + } + }, + { + name: 'MediumPurple', + hex: '9370DB', + rgb: { + r: 147, + g: 112, + b: 219 + } + }, + { + name: 'BlueViolet', + hex: '8A2BE2', + rgb: { + r: 138, + g: 43, + b: 226 + } + }, + { + name: 'DarkViolet', + hex: '9400D3', + rgb: { + r: 148, + g: 0, + b: 211 + } + }, + { + name: 'DarkOrchid', + hex: '9932CC', + rgb: { + r: 153, + g: 50, + b: 204 + } + }, + { + name: 'DarkMagenta', + hex: '8B008B', + rgb: { + r: 139, + g: 0, + b: 139 + } + }, + { + name: 'Purple', + hex: '800080', + rgb: { + r: 128, + g: 0, + b: 128 + } + }, + { + name: 'Indigo', + hex: '4B0082', + rgb: { + r: 75, + g: 0, + b: 130 + } + }, + { + name: 'DarkSlateBlue', + hex: '483D8B', + rgb: { + r: 72, + g: 61, + b: 139 + } + }, + { + name: 'SlateBlue', + hex: '6A5ACD', + rgb: { + r: 106, + g: 90, + b: 205 + } + }, + { + name: 'MediumSlateBlue', + hex: '7B68EE', + rgb: { + r: 123, + g: 104, + b: 238 + } + }, + { + name: 'Pink', + hex: 'FFC0CB', + rgb: { + r: 255, + g: 192, + b: 203 + } + }, + { + name: 'LightPink', + hex: 'FFB6C1', + rgb: { + r: 255, + g: 182, + b: 193 + } + }, + { + name: 'HotPink', + hex: 'FF69B4', + rgb: { + r: 255, + g: 105, + b: 180 + } + }, + { + name: 'DeepPink', + hex: 'FF1493', + rgb: { + r: 255, + g: 20, + b: 147 + } + }, + { + name: 'PaleVioletRed', + hex: 'DB7093', + rgb: { + r: 219, + g: 112, + b: 147 + } + }, + { + name: 'MediumVioletRed', + hex: 'C71585', + rgb: { + r: 199, + g: 21, + b: 133 + } + }, + { + name: 'LightSalmon', + hex: 'FFA07A', + rgb: { + r: 255, + g: 160, + b: 122 + } + }, + { + name: 'Salmon', + hex: 'FA8072', + rgb: { + r: 250, + g: 128, + b: 114 + } + }, + { + name: 'DarkSalmon', + hex: 'E9967A', + rgb: { + r: 233, + g: 150, + b: 122 + } + }, + { + name: 'LightCoral', + hex: 'F08080', + rgb: { + r: 240, + g: 128, + b: 128 + } + }, + { + name: 'IndianRed', + hex: 'CD5C5C', + rgb: { + r: 205, + g: 92, + b: 92 + } + }, + { + name: 'Crimson', + hex: 'DC143C', + rgb: { + r: 220, + g: 20, + b: 60 + } + }, + { + name: 'FireBrick', + hex: 'B22222', + rgb: { + r: 178, + g: 34, + b: 34 + } + }, + { + name: 'DarkRed', + hex: '8B0000', + rgb: { + r: 139, + g: 0, + b: 0 + } + }, + { + name: 'Red', + hex: 'FF0000', + rgb: { + r: 255, + g: 0, + b: 0 + } + }, + { + name: 'OrangeRed', + hex: 'FF4500', + rgb: { + r: 255, + g: 69, + b: 0 + } + }, + { + name: 'Tomato', + hex: 'FF6347', + rgb: { + r: 255, + g: 99, + b: 71 + } + }, + { + name: 'Coral', + hex: 'FF7F50', + rgb: { + r: 255, + g: 127, + b: 80 + } + }, + { + name: 'DarkOrange', + hex: 'FF8C00', + rgb: { + r: 255, + g: 140, + b: 0 + } + }, + { + name: 'Orange', + hex: 'FFA500', + rgb: { + r: 255, + g: 165, + b: 0 + } + }, + { + name: 'Yellow', + hex: 'FFFF00', + rgb: { + r: 255, + g: 255, + b: 0 + } + }, + { + name: 'LightYellow', + hex: 'FFFFE0', + rgb: { + r: 255, + g: 255, + b: 224 + } + }, + { + name: 'LemonChiffon', + hex: 'FFFACD', + rgb: { + r: 255, + g: 250, + b: 205 + } + }, + { + name: 'LightGoldenrodYellow', + hex: 'FAFAD2', + rgb: { + r: 250, + g: 250, + b: 210 + } + }, + { + name: 'PapayaWhip', + hex: 'FFEFD5', + rgb: { + r: 255, + g: 239, + b: 213 + } + }, + { + name: 'Moccasin', + hex: 'FFE4B5', + rgb: { + r: 255, + g: 228, + b: 181 + } + }, + { + name: 'PeachPuff', + hex: 'FFDAB9', + rgb: { + r: 255, + g: 218, + b: 185 + } + }, + { + name: 'PaleGoldenrod', + hex: 'EEE8AA', + rgb: { + r: 238, + g: 232, + b: 170 + } + }, + { + name: 'Khaki', + hex: 'F0E68C', + rgb: { + r: 240, + g: 230, + b: 140 + } + }, + { + name: 'DarkKhaki', + hex: 'BDB76B', + rgb: { + r: 189, + g: 183, + b: 107 + } + }, + { + name: 'Gold', + hex: 'FFD700', + rgb: { + r: 255, + g: 215, + b: 0 + } + }, + { + name: 'Cornsilk', + hex: 'FFF8DC', + rgb: { + r: 255, + g: 248, + b: 220 + } + }, + { + name: 'BlanchedAlmond', + hex: 'FFEBCD', + rgb: { + r: 255, + g: 235, + b: 205 + } + }, + { + name: 'Bisque', + hex: 'FFE4C4', + rgb: { + r: 255, + g: 228, + b: 196 + } + }, + { + name: 'NavajoWhite', + hex: 'FFDEAD', + rgb: { + r: 255, + g: 222, + b: 173 + } + }, + { + name: 'Wheat', + hex: 'F5DEB3', + rgb: { + r: 245, + g: 222, + b: 179 + } + }, + { + name: 'BurlyWood', + hex: 'DEB887', + rgb: { + r: 222, + g: 184, + b: 135 + } + }, + { + name: 'Tan', + hex: 'D2B48C', + rgb: { + r: 210, + g: 180, + b: 140 + } + }, + { + name: 'RosyBrown', + hex: 'BC8F8F', + rgb: { + r: 188, + g: 143, + b: 143 + } + }, + { + name: 'SandyBrown', + hex: 'F4A460', + rgb: { + r: 244, + g: 164, + b: 96 + } + }, + { + name: 'Goldenrod', + hex: 'DAA520', + rgb: { + r: 218, + g: 165, + b: 32 + } + }, + { + name: 'DarkGoldenrod', + hex: 'B8860B', + rgb: { + r: 184, + g: 134, + b: 11 + } + }, + { + name: 'Peru', + hex: 'CD853F', + rgb: { + r: 205, + g: 133, + b: 63 + } + }, + { + name: 'Chocolate', + hex: 'D2691E', + rgb: { + r: 210, + g: 105, + b: 30 + } + }, + { + name: 'SaddleBrown', + hex: '8B4513', + rgb: { + r: 139, + g: 69, + b: 19 + } + }, + { + name: 'Sienna', + hex: 'A0522D', + rgb: { + r: 160, + g: 82, + b: 45 + } + }, + { + name: 'Brown', + hex: 'A52A2A', + rgb: { + r: 165, + g: 42, + b: 42 + } + }, + { + name: 'Maroon', + hex: '800000', + rgb: { + r: 128, + g: 0, + b: 0 + } + }, + { + name: 'DarkOliveGreen', + hex: '556B2F', + rgb: { + r: 85, + g: 107, + b: 47 + } + }, + { + name: 'Olive', + hex: '808000', + rgb: { + r: 128, + g: 128, + b: 0 + } + }, + { + name: 'OliveDrab', + hex: '6B8E23', + rgb: { + r: 107, + g: 142, + b: 35 + } + }, + { + name: 'YellowGreen', + hex: '9ACD32', + rgb: { + r: 154, + g: 205, + b: 50 + } + }, + { + name: 'LimeGreen', + hex: '32CD32', + rgb: { + r: 50, + g: 205, + b: 50 + } + }, + { + name: 'Lime', + hex: '00FF00', + rgb: { + r: 0, + g: 255, + b: 0 + } + }, + { + name: 'LawnGreen', + hex: '7CFC00', + rgb: { + r: 124, + g: 252, + b: 0 + } + }, + { + name: 'Chartreuse', + hex: '7FFF00', + rgb: { + r: 127, + g: 255, + b: 0 + } + }, + { + name: 'GreenYellow', + hex: 'ADFF2F', + rgb: { + r: 173, + g: 255, + b: 47 + } + }, + { + name: 'SpringGreen', + hex: '00FF7F', + rgb: { + r: 0, + g: 255, + b: 127 + } + }, + { + name: 'MediumSpringGreen', + hex: '00FA9A', + rgb: { + r: 0, + g: 250, + b: 154 + } + }, + { + name: 'LightGreen', + hex: '90EE90', + rgb: { + r: 144, + g: 238, + b: 144 + } + }, + { + name: 'PaleGreen', + hex: '98FB98', + rgb: { + r: 152, + g: 251, + b: 152 + } + }, + { + name: 'DarkSeaGreen', + hex: '8FBC8F', + rgb: { + r: 143, + g: 188, + b: 143 + } + }, + { + name: 'MediumSeaGreen', + hex: '3CB371', + rgb: { + r: 60, + g: 179, + b: 113 + } + }, + { + name: 'SeaGreen', + hex: '2E8B57', + rgb: { + r: 46, + g: 139, + b: 87 + } + }, + { + name: 'ForestGreen', + hex: '228B22', + rgb: { + r: 34, + g: 139, + b: 34 + } + }, + { + name: 'Green', + hex: '008000', + rgb: { + r: 0, + g: 128, + b: 0 + } + }, + { + name: 'DarkGreen', + hex: '006400', + rgb: { + r: 0, + g: 100, + b: 0 + } + }, + { + name: 'MediumAquamarine', + hex: '66CDAA', + rgb: { + r: 102, + g: 205, + b: 170 + } + }, + { + name: 'Aqua', + hex: '00FFFF', + rgb: { + r: 0, + g: 255, + b: 255 + } + }, + { + name: 'Cyan', + hex: '00FFFF', + rgb: { + r: 0, + g: 255, + b: 255 + } + }, + { + name: 'LightCyan', + hex: 'E0FFFF', + rgb: { + r: 224, + g: 255, + b: 255 + } + }, + { + name: 'PaleTurquoise', + hex: 'AFEEEE', + rgb: { + r: 175, + g: 238, + b: 238 + } + }, + { + name: 'Aquamarine', + hex: '7FFFD4', + rgb: { + r: 127, + g: 255, + b: 212 + } + }, + { + name: 'Turquoise', + hex: '40E0D0', + rgb: { + r: 64, + g: 224, + b: 208 + } + }, + { + name: 'MediumTurquoise', + hex: '48D1CC', + rgb: { + r: 72, + g: 209, + b: 204 + } + }, + { + name: 'DarkTurquoise', + hex: '00CED1', + rgb: { + r: 0, + g: 206, + b: 209 + } + }, + { + name: 'LightSeaGreen', + hex: '20B2AA', + rgb: { + r: 32, + g: 178, + b: 170 + } + }, + { + name: 'CadetBlue', + hex: '5F9EA0', + rgb: { + r: 95, + g: 158, + b: 160 + } + }, + { + name: 'DarkCyan', + hex: '008B8B', + rgb: { + r: 0, + g: 139, + b: 139 + } + }, + { + name: 'Teal', + hex: '008080', + rgb: { + r: 0, + g: 128, + b: 128 + } + }, + { + name: 'LightSteelBlue', + hex: 'B0C4DE', + rgb: { + r: 176, + g: 196, + b: 222 + } + }, + { + name: 'PowderBlue', + hex: 'B0E0E6', + rgb: { + r: 176, + g: 224, + b: 230 + } + }, + { + name: 'LightBlue', + hex: 'ADD8E6', + rgb: { + r: 173, + g: 216, + b: 230 + } + }, + { + name: 'SkyBlue', + hex: '87CEEB', + rgb: { + r: 135, + g: 206, + b: 235 + } + }, + { + name: 'LightSkyBlue', + hex: '87CEFA', + rgb: { + r: 135, + g: 206, + b: 250 + } + }, + { + name: 'DeepSkyBlue', + hex: '00BFFF', + rgb: { + r: 0, + g: 191, + b: 255 + } + }, + { + name: 'DodgerBlue', + hex: '1E90FF', + rgb: { + r: 30, + g: 144, + b: 255 + } + }, + { + name: 'CornflowerBlue', + hex: '6495ED', + rgb: { + r: 100, + g: 149, + b: 237 + } + }, + { + name: 'SteelBlue', + hex: '4682B4', + rgb: { + r: 70, + g: 130, + b: 180 + } + }, + { + name: 'RoyalBlue', + hex: '4169E1', + rgb: { + r: 65, + g: 105, + b: 225 + } + }, + { + name: 'Blue', + hex: '0000FF', + rgb: { + r: 0, + g: 0, + b: 255 + } + }, + { + name: 'MediumBlue', + hex: '0000CD', + rgb: { + r: 0, + g: 0, + b: 205 + } + }, + { + name: 'DarkBlue', + hex: '00008B', + rgb: { + r: 0, + g: 0, + b: 139 + } + }, + { + name: 'Navy', + hex: '000080', + rgb: { + r: 0, + g: 0, + b: 128 + } + }, + { + name: 'MidnightBlue', + hex: '191970', + rgb: { + r: 25, + g: 25, + b: 112 + } + }, + { + name: 'White', + hex: 'FFFFFF', + rgb: { + r: 255, + g: 255, + b: 255 + } + }, + { + name: 'Snow', + hex: 'FFFAFA', + rgb: { + r: 255, + g: 250, + b: 250 + } + }, + { + name: 'Honeydew', + hex: 'F0FFF0', + rgb: { + r: 240, + g: 255, + b: 240 + } + }, + { + name: 'MintCream', + hex: 'F5FFFA', + rgb: { + r: 245, + g: 255, + b: 250 + } + }, + { + name: 'Azure', + hex: 'F0FFFF', + rgb: { + r: 240, + g: 255, + b: 255 + } + }, + { + name: 'AliceBlue', + hex: 'F0F8FF', + rgb: { + r: 240, + g: 248, + b: 255 + } + }, + { + name: 'GhostWhite', + hex: 'F8F8FF', + rgb: { + r: 248, + g: 248, + b: 255 + } + }, + { + name: 'WhiteSmoke', + hex: 'F5F5F5', + rgb: { + r: 245, + g: 245, + b: 245 + } + }, + { + name: 'Seashell', + hex: 'FFF5EE', + rgb: { + r: 255, + g: 245, + b: 238 + } + }, + { + name: 'Beige', + hex: 'F5F5DC', + rgb: { + r: 245, + g: 245, + b: 220 + } + }, + { + name: 'OldLace', + hex: 'FDF5E6', + rgb: { + r: 253, + g: 245, + b: 230 + } + }, + { + name: 'FloralWhite', + hex: 'FFFAF0', + rgb: { + r: 255, + g: 250, + b: 240 + } + }, + { + name: 'Ivory', + hex: 'FFFFF0', + rgb: { + r: 255, + g: 255, + b: 240 + } + }, + { + name: 'AntiqueWhite', + hex: 'FAEBD7', + rgb: { + r: 250, + g: 235, + b: 215 + } + }, + { + name: 'Linen', + hex: 'FAF0E6', + rgb: { + r: 250, + g: 240, + b: 230 + } + }, + { + name: 'LavenderBlush', + hex: 'FFF0F5', + rgb: { + r: 255, + g: 240, + b: 245 + } + }, + { + name: 'MistyRose', + hex: 'FFE4E1', + rgb: { + r: 255, + g: 228, + b: 225 + } + }, + { + name: 'Gainsboro', + hex: 'DCDCDC', + rgb: { + r: 220, + g: 220, + b: 220 + } + }, + { + name: 'LightGray', + hex: 'D3D3D3', + rgb: { + r: 211, + g: 211, + b: 211 + } + }, + { + name: 'Silver', + hex: 'C0C0C0', + rgb: { + r: 192, + g: 192, + b: 192 + } + }, + { + name: 'DarkGray', + hex: 'A9A9A9', + rgb: { + r: 169, + g: 169, + b: 169 + } + }, + { + name: 'Gray', + hex: '808080', + rgb: { + r: 128, + g: 128, + b: 128 + } + }, + { + name: 'DimGray', + hex: '696969', + rgb: { + r: 105, + g: 105, + b: 105 + } + }, + { + name: 'LightSlateGray', + hex: '778899', + rgb: { + r: 119, + g: 136, + b: 153 + } + }, + { + name: 'SlateGray', + hex: '708090', + rgb: { + r: 112, + g: 128, + b: 144 + } + }, + { + name: 'DarkSlateGray', + hex: '2F4F4F', + rgb: { + r: 47, + g: 79, + b: 79 + } + }, + { + name: 'Black', + hex: '000000', + rgb: { + r: 0, + g: 0, + b: 0 + } + } + ]; +} diff --git a/projects/ngx-coding-components/codebook-models/ng-package.json b/projects/ngx-coding-components/codebook-models/ng-package.json new file mode 100644 index 0000000..ed27894 --- /dev/null +++ b/projects/ngx-coding-components/codebook-models/ng-package.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-package.schema.json", + "lib": { + "entryFile": "public-api.ts" + } +} diff --git a/projects/ngx-coding-components/codebook-models/public-api.ts b/projects/ngx-coding-components/codebook-models/public-api.ts new file mode 100644 index 0000000..1b21f73 --- /dev/null +++ b/projects/ngx-coding-components/codebook-models/public-api.ts @@ -0,0 +1,155 @@ +import { VariableInfo } from '@iqbspecs/variable-info/variable-info.interface'; + +/** + * Item metadata for codebook + */ +export interface ItemMetadata { + id: string; + variableId: string; +} + +/** + * Settings for codebook content generation + */ +export interface CodeBookContentSetting { + /** Export format (docx or json) */ + exportFormat: 'json' | 'docx'; + /** Missings profile name */ + missingsProfile: string; + /** Include only manual coding */ + hasOnlyManualCoding: boolean; + /** Include general instructions */ + hasGeneralInstructions: boolean; + /** Include derived variables */ + hasDerivedVars: boolean; + /** Include only variables with codes */ + hasOnlyVarsWithCodes: boolean; + /** Include closed variables */ + hasClosedVars: boolean; + /** Convert code labels to uppercase */ + codeLabelToUpper: boolean; + /** Show score */ + showScore: boolean; + /** Hide item-variable relation */ + hideItemVarRelation: boolean; +} + +/** + * Missing code definition + */ +export interface Missing { + /** Missing code */ + code: string | number; + /** Missing label */ + label: string; + /** Missing description */ + description: string; +} + +/** + * Code information for codebook + */ +export interface CodeInfo { + /** Code ID */ + id: string; + /** Code label */ + label: string; + /** Code description */ + description: string; + /** Code score (optional) */ + score?: string; +} + +/** + * Variable information for codebook + */ +export interface BookVariable { + /** Variable ID */ + id: string; + /** Variable label */ + label: string; + /** Variable source type */ + sourceType: string; + /** General instruction */ + generalInstruction: string; + /** Codes */ + codes: CodeInfo[]; +} + +/** + * Unit data for codebook + */ +export interface CodebookUnitDto { + /** Unit key */ + key: string; + /** Unit name */ + name: string; + /** Variables */ + variables: BookVariable[]; + /** Missings */ + missings: Missing[]; + /** Items (optional) */ + items?: ItemMetadata[]; +} + +/** + * Unit properties for codebook generation + */ +export interface UnitPropertiesForCodebook { + /** Unit ID */ + id: number; + /** Unit key */ + key: string; + /** Unit name */ + name: string; + /** Coding scheme */ + scheme?: string; + /** Scheme type */ + schemeType?: string; + /** Metadata */ + metadata?: { + /** Items */ + items?: ItemMetadata[]; + }; + /** Variables */ + variables?: VariableInfo[]; +} + +/** + * Unit selection item for codebook export UI + */ +export interface UnitSelectionItem { + /** Unit ID */ + unitId: number; + /** Unit name */ + unitName: string; + /** Unit alias */ + unitAlias?: string | null; + key: string; + groupName?: string; + disabled?: boolean; +} + +/** + * Missings profile for selection + */ +export interface MissingsProfile { + /** Profile ID */ + id: number; + /** Profile label */ + label: string; + /** Missings data */ + missings?: Missing[] | string; +} + +/** + * Codebook export configuration + */ +export interface CodebookExportConfig { + /** Selected unit IDs */ + selectedUnits: number[]; + /** Content options */ + contentOptions: CodeBookContentSetting; + /** Selected missings profile ID */ + missingsProfileId: number; +} diff --git a/projects/ngx-coding-components/ng-package.json b/projects/ngx-coding-components/ng-package.json index ee0f04d..5b04d68 100644 --- a/projects/ngx-coding-components/ng-package.json +++ b/projects/ngx-coding-components/ng-package.json @@ -3,7 +3,13 @@ "dest": "../../dist/ngx-coding-components", "allowedNonPeerDependencies": [ "@iqb/mathlive", - "katex" + "katex", + "docx", + "cheerio", + "buffer", + "image-size", + "mathml2omml", + "@iqb/responses" ], "lib": { "entryFile": "src/public-api.ts" diff --git a/projects/ngx-coding-components/package.json b/projects/ngx-coding-components/package.json index 6c753d2..9097043 100644 --- a/projects/ngx-coding-components/package.json +++ b/projects/ngx-coding-components/package.json @@ -4,8 +4,8 @@ "exports": { "./lib/ngx-coding-components.module": "./lib/ngx-coding-components.module.d.ts" }, - "version": "4.0.0", - "author": "IQB - Institut zur Qualitätsentwicklung im Bildungswesen", + "version": "4.1.0", + "author": "IQB - Institut zur Qualit\u00e4tsentwicklung im Bildungswesen", "license": "MIT", "description": "Angular components for coding responses in assessments of IQB.", "repository": { @@ -29,9 +29,8 @@ "@angular/platform-browser": "^21.0.0", "@angular/platform-browser-dynamic": "^21.0.0", "@angular/router": "^21.0.0", - "@iqb/responses": "^5.2.2", "@iqbspecs/coding-scheme": "^3.4.1", - "@iqbspecs/response": "^2.0.0", + "@iqbspecs/response": "^1.5.1 || ^2.0.0", "@iqbspecs/variable-info": "^1.3.0", "@ngx-translate/core": "^15.0.0", "@tiptap/core": "^2.23.1", @@ -72,7 +71,26 @@ "dependencies": { "@iqb/mathlive": "^0.4.1", "katex": "^0.16.11", - "tslib": "^2.3.0" + "tslib": "^2.3.0", + "docx": "8.5.0", + "cheerio": "1.1.2", + "buffer": "6.0.3", + "image-size": "1.2.1", + "mathml2omml": "0.5.0", + "@iqb/responses": "5.2.2" }, - "sideEffects": false + "sideEffects": false, + "typesVersions": { + "*": { + "codebook-models": [ + "types/iqb-ngx-coding-components-codebook-models.d.ts" + ], + "codebook-generator": [ + "types/iqb-ngx-coding-components-codebook-generator.d.ts" + ], + "codebook-export": [ + "types/iqb-ngx-coding-components-codebook-export.d.ts" + ] + } + } } diff --git a/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts b/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts new file mode 100644 index 0000000..edba782 --- /dev/null +++ b/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts @@ -0,0 +1,60 @@ +import { SimpleChange } from '@angular/core'; +import { CodebookExportComponent } from '../../../codebook-export/codebook-export.component'; + +describe('Studio codebook selection contract', () => { + let component: CodebookExportComponent; + beforeEach(() => { + component = new CodebookExportComponent(); + component.availableUnits = [ + { + unitId: 1, key: 'A', unitName: 'Alpha', groupName: 'Gruppe' + }, + { unitId: 2, key: 'B', unitName: 'Beta' }, + { + unitId: 3, key: 'C', unitName: 'Gesperrt', disabled: true + } + ]; + component.ngOnChanges({ availableUnits: new SimpleChange(null, component.availableUnits, true) }); + }); + + it('selects all available units across search results, excluding disabled units', () => { + component.filter = 'Alpha'; component.applyFilter(); component.toggleAll(); + expect(component.selection).toEqual([1, 2]); + expect(component.allSelected).toBeTrue(); + component.toggleAll(); expect(component.selection).toEqual([]); + }); + + it('searches names, keys and groups without changing selection', () => { + component.toggle(component.availableUnits[1]); + component.filter = 'gruppe'; component.applyFilter(); + expect(component.selection).toEqual([2]); + expect(component.dataSource.filteredData.map(unit => unit.unitId)).toEqual([1]); + }); + + it('accepts initial selection and removes unavailable or disabled IDs', () => { + component.selectedUnitIds = [1, 3, 999]; + component.ngOnChanges({ selectedUnitIds: new SimpleChange([], component.selectedUnitIds, true) }); + expect(component.selection).toEqual([1]); + }); + + it('emits a copied export configuration without starting a download or a job', () => { + component.toggle(component.availableUnits[0]); + component.missingsProfiles = [{ id: 8, label: 'Profil' }]; component.selectedMissingsProfileId = 8; + const emit = spyOn(component.exportRequested, 'emit'); + component.exportCodingBook(); + expect(emit).toHaveBeenCalledWith(jasmine.objectContaining({ selectedUnits: [1], missingsProfileId: 8 })); + const config = emit.calls.mostRecent().args[0]!; + component.contentOptions.showScore = false; + expect(config.contentOptions.showScore).toBeTrue(); + expect(config.contentOptions.missingsProfile).toBe('Profil'); + }); + + it('blocks export for empty selection, loading, busy and unsaved changes', () => { + const emit = spyOn(component.exportRequested, 'emit'); + component.exportCodingBook(); component.toggle(component.availableUnits[0]); + component.loading = true; component.exportCodingBook(); component.loading = false; + component.busy = true; component.exportCodingBook(); component.busy = false; + component.workspaceChanges = true; component.exportCodingBook(); + expect(emit).not.toHaveBeenCalled(); + }); +}); diff --git a/projects/ngx-coding-components/tsconfig.spec.json b/projects/ngx-coding-components/tsconfig.spec.json index 2432ead..fc50d18 100644 --- a/projects/ngx-coding-components/tsconfig.spec.json +++ b/projects/ngx-coding-components/tsconfig.spec.json @@ -7,6 +7,9 @@ "jasmine" ], "paths": { + "@iqb/ngx-coding-components/codebook-export": ["projects/ngx-coding-components/codebook-export/public-api.ts"], + "@iqb/ngx-coding-components/codebook-generator": ["projects/ngx-coding-components/codebook-generator/public-api.ts"], + "@iqb/ngx-coding-components/codebook-models": ["projects/ngx-coding-components/codebook-models/public-api.ts"], "@angular/*": ["../../node_modules/@angular/*"], "@ngx-translate/core": ["../../node_modules/@ngx-translate/core"], "@ngx-translate/core/*": ["../../node_modules/@ngx-translate/core/*"], diff --git a/scripts/build-codebook-node.mjs b/scripts/build-codebook-node.mjs new file mode 100644 index 0000000..63ef6d7 --- /dev/null +++ b/scripts/build-codebook-node.mjs @@ -0,0 +1,18 @@ +import { build } from 'esbuild'; +import { readFile, writeFile, copyFile } from 'node:fs/promises'; + +// Nest/webpack and Jest consumers also need a native CommonJS entry point. +const root = 'dist/ngx-coding-components'; +await build({ + entryPoints: ['projects/ngx-coding-components/codebook-generator/public-api.ts'], + outfile: `${root}/cjs/codebook-generator.cjs`, bundle: true, platform: 'node', + format: 'cjs', target: 'node20', external: ['@iqb/responses', '@iqbspecs/coding-scheme', 'docx', 'cheerio', 'buffer', 'image-size', 'katex'], sourcemap: true +}); +const manifest = JSON.parse(await readFile(`${root}/package.json`, 'utf8')); +const entry = manifest.exports['./codebook-generator']; +manifest.exports['./codebook-generator'] = { + types: entry.types, require: './cjs/codebook-generator.cjs', default: entry.default +}; +await writeFile(`${root}/package.json`, `${JSON.stringify(manifest, null, 2)}\n`); + +await copyFile('node_modules/mathml2omml/LICENSE', `${root}/cjs/mathml2omml.LICENSE`); diff --git a/scripts/codebook-contract.test.mjs b/scripts/codebook-contract.test.mjs new file mode 100644 index 0000000..302756c --- /dev/null +++ b/scripts/codebook-contract.test.mjs @@ -0,0 +1,137 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { createRequire } from 'node:module'; +import JSZip from 'jszip'; +import { CodebookGenerator, CodebookDocxGenerator, hasCodebookManualInstruction } from '../dist/ngx-coding-components/fesm2022/iqb-ngx-coding-components-codebook-generator.mjs'; + +const options = { exportFormat: 'json', missingsProfile: '', hasOnlyManualCoding: false, + hasClosedVars: false, hasOnlyVarsWithCodes: true, hasDerivedVars: true, + hasGeneralInstructions: true, codeLabelToUpper: false, showScore: true, hideItemVarRelation: false }; +const code = (id, manualInstruction = '', type = 'FULL_CREDIT') => ({ id, label: `Code ${id}`, manualInstruction, type, score: 1, ruleSets: [], ruleSetOperatorAnd: true }); +const variable = (id, codes, extra = {}) => ({ id, alias: id, label: id, sourceType: 'BASE', manualInstruction: '', codes, ...extra }); +const unit = (variables, key = 'U') => ({ id: 1, key, name: 'Aufgabe', scheme: JSON.stringify({ version: '3.0', variableCodings: variables }), metadata: { items: [{ id: 'ITEM', variableId: 'V' }] } }); +const data = (variables, extra = {}) => CodebookGenerator.getCodebookData([unit(variables)], { ...options, ...extra }, [])[0].variables; + +for (const manual of [false, true]) for (const closed of [false, true]) for (const onlyCodes of [false, true]) { + test(`selection manual=${manual} closed=${closed} onlyCodes=${onlyCodes}`, () => { + const vars = [variable('M', [code(1, '

Bewerten

'), code(2)]), variable('C', [code(0, '', 'RESIDUAL_AUTO')]), + variable('X', [code(1, 'Bewerten'), code(0, '', 'RESIDUAL_AUTO')]), variable('A', [code(1)]), variable('E', [])]; + const actual = data(vars, { hasOnlyManualCoding: manual, hasClosedVars: closed, hasOnlyVarsWithCodes: onlyCodes }); + const expected = manual && closed ? ['C', 'M', 'X'] : manual ? ['M', 'X'] : closed ? ['C', 'X'] : onlyCodes ? ['A', 'C', 'M', 'X'] : ['A', 'C', 'E', 'M', 'X']; + assert.deepEqual(actual.map(v => v.id), expected); + if (manual && !closed) assert.deepEqual(actual.map(v => v.codes.map(c => c.id)), [['1'], ['1']]); + }); +} + +test('manual rich text handles empty HTML, invisible text, image and formula-only content', () => { + for (const empty of ['', ' ', '

 ​

', '


', '']) assert.equal(hasCodebookManualInstruction(empty), false, empty); + for (const nonempty of ['Text', '

<

', '', '', '[[iqb-math:x^2]]']) assert.equal(hasCodebookManualInstruction(nonempty), true, nonempty); +}); + +test('IDs, code zero, derived filtering, BASE_NO_VALUE and stable sorting', () => { + const vars = [variable('Z', [code(0)], { alias: 'A' }), variable('B', [code(-1)]), variable('NO', [code(1)], { sourceType: 'BASE_NO_VALUE' }), variable('D', [code(1)], { sourceType: 'COPY_VALUE' })]; + const actual = data(vars, { hasDerivedVars: false }); + assert.deepEqual(actual.map(v => v.id), ['A', 'B']); + assert.equal(actual[0].codes[0].id, '0'); +}); + +test('invalid schemas fail with the unit key and absent schemes are allowed', () => { + assert.throws(() => CodebookGenerator.getCodebookData([{ id: 1, key: 'BAD', name: '', scheme: '{' }], options, []), /BAD/); + assert.deepEqual(CodebookGenerator.getCodebookData([{ id: 1, key: 'EMPTY', name: '' }], options, [])[0].variables, []); +}); + +test('JSON contract omits items and score when disabled; units sorted by key', async () => { + const blob = await CodebookGenerator.generateCodebook([unit([variable('V', [code(0)])], 'Z'), unit([], 'A')], { ...options, showScore: false }, [{ code: 0, label: 'Missing', description: 'Leer' }]); + const parsed = JSON.parse(await blob.text()); + assert.deepEqual(parsed.map(u => u.key), ['A', 'Z']); + assert.equal('items' in parsed[1], false); + assert.equal('score' in parsed[1].variables[0].codes[0], false); + assert.equal(parsed[1].missings[0].code, 0); +}); + +async function document(description, extra = {}) { + const units = [{ key: 'U', name: 'Aufgabe', variables: [{ id: 'V', label: 'Variable', sourceType: 'BASE', generalInstruction: '', codes: [{ id: '0', label: 'CODE ZERO', score: '7', description }] }], + items: [{ id: 'ITEM', variableId: 'V' }], missings: [{ code: 0, label: 'MISSING ZERO', description: 'Leer' }] }]; + const blob = await CodebookDocxGenerator.generateDocx(units, { ...options, ...extra }); + const zip = await JSZip.loadAsync(await blob.arrayBuffer()); + return { zip, xml: await zip.file('word/document.xml').async('string') }; +} + +test('DOCX keeps Studio headings, item relations, score column order and neutral footer', async () => { + const { zip, xml } = await document('

INSTRUCTION

'); + assert.match(xml, /U Aufgabe/); assert.match(xml, /V Variable/); assert.match(xml, /Item\(s\): ITEM/); + assert.match(xml, /0 MISSING ZERO/); + assert.ok(xml.indexOf('>7<') < xml.indexOf('INSTRUCTION')); + const footer = await zip.file('word/footer1.xml').async('string'); + assert.match(footer, /IQB Codebook/); assert.doesNotMatch(footer, /IQB-Studio|IQB-Kodierbox/); +}); + +for (const description of ['

', '

[[iqb-math:x^2]]

']) { + test(`DOCX retains mathematical content ${description}`, async () => { + const { xml } = await document(description); + assert.match(xml, / { + const image = 'iVBORw0KGgoAAAANSUhEUgAAACgAAAAUCAIAAABwJOjsAAAAJElEQVR4nGOQSDgwIIhh1OJRi0ctHrV41OJRi0ctHrV45FgMAHBlzy43YLJ+AAAAAElFTkSuQmCC'; + const { zip, xml } = await document(`

unterdurch
neu

`); + assert.match(xml, / name.startsWith('word/media/'))); +}); + +test('empty exports produce JSON array and an actual DOCX archive', async () => { + assert.equal(await (await CodebookGenerator.generateCodebook([], options, [])).text(), '[]'); + const blob = await CodebookGenerator.generateCodebook([], { ...options, exportFormat: 'docx' }, []); + const zip = await JSZip.loadAsync(await blob.arrayBuffer()); + assert.ok(zip.file('word/document.xml')); +}); + +test('CommonJS and ESM backend entry points agree without Angular or browser globals', async () => { + const require = createRequire(import.meta.url); + const commonjs = require('../dist/ngx-coding-components/cjs/codebook-generator.cjs'); + const units = [unit([variable('V', [code(0, 'Manuell')])])]; + const a = await CodebookGenerator.generateCodebook(units, options, []); + const b = await commonjs.CodebookGenerator.generateCodebook(units, options, []); + assert.equal(await a.text(), await b.text()); +}); + +test('plain instructions and whitespace around formatted text are not lost', async () => { + const plain = await document('Bitte bewerten'); assert.match(plain.xml, /Bitte bewerten/); + const { xml } = await document('

vor fett nach

'); + assert.match(xml, />vor nach { + const { xml } = await document('

Prefix [[iqb-math:a%3Cb]] suffix

'); + assert.match(xml, /a<b/); assert.doesNotMatch(xml, //); + assert.doesNotMatch(xml, //); +}); + +test('incomplete codes are closed and general instructions alone are not manual', () => { + const variables = [variable('I', [code(0, '', 'INTENDED_INCOMPLETE')]), + variable('G', [code(1)], { manualInstruction: '

Allgemeiner Hinweis

' })]; + assert.deepEqual(data(variables, { hasClosedVars: true }).map(v => v.id), ['I']); + assert.deepEqual(data(variables, { hasOnlyManualCoding: true }), []); +}); + +test('malformed variable and code collections produce a named export error', () => { + for (const variables of [[null], [{ id: 'V', codes: {} }], [{ id: 'V', codes: [null] }]]) { + assert.throws(() => CodebookGenerator.getCodebookData([unit(variables, 'INVALID')], options, []), /INVALID/); + } +}); + +test('Studio reference preserves document, styles, numbering, settings and page footer XML', async () => { + const { readFile } = await import('node:fs/promises'); + const units = [{ key: 'A', name: 'Referenz', variables: [{ id: 'V', label: 'Variable', sourceType: 'BASE', + generalInstruction: '

Hinweis

', codes: [{ id: '1', label: 'RICHTIG', score: '1', + description: '

Text

[[iqb-math:x^2]]

' }] }], + missings: [{ code: 99, label: 'MISSING', description: 'Keine Antwort' }], items: [] }]; + const blob = await CodebookDocxGenerator.generateDocx(units, { ...options, exportFormat: 'docx', codeLabelToUpper: true, hideItemVarRelation: true }); + const zip = await JSZip.loadAsync(await blob.arrayBuffer()); + for (const file of ['document.xml', 'styles.xml', 'numbering.xml', 'settings.xml', 'footer1.xml']) { + const expected = await readFile(new URL(`./fixtures/studio-codebook/${file}`, import.meta.url), 'utf8'); + const actual = (await zip.file(`word/${file}`).async('string')).replace(new Date().toLocaleDateString(), '{{DATE}}'); + assert.equal(actual, expected, file); + } +}); diff --git a/scripts/fixtures/studio-codebook/README.md b/scripts/fixtures/studio-codebook/README.md new file mode 100644 index 0000000..ff24e02 --- /dev/null +++ b/scripts/fixtures/studio-codebook/README.md @@ -0,0 +1,13 @@ +# Studio DOCX reference + +Generated from iqb-berlin/studio-lite develop commit +`2504265721a59c7c65425072951ce82ab16f0fb8`, class `DownloadDocx`, using docx 8.5.0. +The input is in the `Studio reference preserves ...` contract test. + +Only two baseline normalizations: +- `IQB-Studio Codebook` becomes the approved neutral `IQB Codebook`. +- The generation date becomes `{{DATE}}`. + +Document XML, styles, numbering, settings and footer must otherwise match exactly. +The fixture deliberately excludes the separately tested approved corrections +(code/missing 0, item relationships, plain instructions and whitespace boundaries). diff --git a/scripts/fixtures/studio-codebook/document.xml b/scripts/fixtures/studio-codebook/document.xml new file mode 100644 index 0000000..bfa3d3e --- /dev/null +++ b/scripts/fixtures/studio-codebook/document.xml @@ -0,0 +1 @@ +99 MISSINGKeine AntwortA ReferenzV VariableHinweis1RICHTIG1Textx2 \ No newline at end of file diff --git a/scripts/fixtures/studio-codebook/footer1.xml b/scripts/fixtures/studio-codebook/footer1.xml new file mode 100644 index 0000000..c63db64 --- /dev/null +++ b/scripts/fixtures/studio-codebook/footer1.xml @@ -0,0 +1 @@ +IQB Codebook {{DATE}} Seite PAGE von NUMPAGES \ No newline at end of file diff --git a/scripts/fixtures/studio-codebook/numbering.xml b/scripts/fixtures/studio-codebook/numbering.xml new file mode 100644 index 0000000..473063e --- /dev/null +++ b/scripts/fixtures/studio-codebook/numbering.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/fixtures/studio-codebook/settings.xml b/scripts/fixtures/studio-codebook/settings.xml new file mode 100644 index 0000000..58b2c76 --- /dev/null +++ b/scripts/fixtures/studio-codebook/settings.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/scripts/fixtures/studio-codebook/styles.xml b/scripts/fixtures/studio-codebook/styles.xml new file mode 100644 index 0000000..74947d2 --- /dev/null +++ b/scripts/fixtures/studio-codebook/styles.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index a4a04f6..40a826f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,6 +14,9 @@ "esModuleInterop": true, "skipLibCheck": true, "paths": { + "@iqb/ngx-coding-components/codebook-export": ["projects/ngx-coding-components/codebook-export/public-api.ts"], + "@iqb/ngx-coding-components/codebook-generator": ["projects/ngx-coding-components/codebook-generator/public-api.ts"], + "@iqb/ngx-coding-components/codebook-models": ["projects/ngx-coding-components/codebook-models/public-api.ts"], "@ngx-translate/core": [ "./node_modules/@ngx-translate/core" ], From be4f1691d52f30791a0c2d240a91ff2ddb95190b Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:28:26 +0200 Subject: [PATCH 02/36] ci: verify shared codebook contracts and document integration --- .github/workflows/ci.yml | 3 ++ docs/shared-codebook.md | 76 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 docs/shared-codebook.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f5f12ad..d9edeba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,9 @@ jobs: CI: true run: npm run test:coverage + - name: Build shared library and verify codebook contracts + run: npm run build_cc && npm run test:codebook + - name: Coding-scheme validation script run: npm run test:scheme diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md new file mode 100644 index 0000000..40c54ac --- /dev/null +++ b/docs/shared-codebook.md @@ -0,0 +1,76 @@ +# Gemeinsames Codebook: Integrationsvertrag + +Studio ist die Referenz für Bedienung und Dokumentlayout. Die drei Einstiegspunkte +`codebook-models`, `codebook-generator` und `codebook-export` trennen Daten, +frameworkfreie Dokumenterstellung und Angular-Oberfläche. Die Bibliothek enthält +keinen Jobvertrag, keine HTTP-Aufrufe und keinen Download-Dienst. + +## Anwendungen + +Studio lädt Aufgaben und Missingprofile, übergibt die aktive Aufgabe und seine +Speichersperre und verarbeitet `exportRequested` mit seinem direkten API-Aufruf. +Die Kodierbox ergänzt ihre eigenen Filter über `[codebookFilters]` und zeigt ihre +Jobzustände über `[codebookStatus]`. Das sind Projektionsflächen ohne Jobsemantik. +`loading` und `busy` sind allgemeine Sperrzustände. Nur die Kodierbox startet Jobs, +fragt Status ab und lädt Ergebnisse. Beim Schließen endet die Statusabfrage; +der Serverjob wird nicht abgebrochen. + +Die Übersetzungen der Oberfläche stehen im Host unter `codebook.*`. Beide Hosts +verwenden dieselben Studio-Beschriftungen und Tooltips. `selectionChanged`, +`exportRequested` und `cancel` sind die einzigen Ausgaben. Der Exportwunsch enthält +Aufgaben-IDs, fachliche Optionen und Missingprofil-ID. Die Profile tragen eine +numerische ID und eine Beschriftung; 0 bedeutet kein Profil. + +## Bewusste fachliche Korrekturen + +* Nur manuell: Variablen mit regulären manuell beschriebenen Codes; nur diese Codes. +* Nur geschlossen: Variablen mit RESIDUAL_AUTO oder INTENDED_INCOMPLETE; alle Codes. +* Beide: Vereinigung dieser Variablengruppen; alle Codes. +* Beide aus: keine Einschränkung nach dieser Kategorie. +* Nur mit Codes wird nach der Codeauswahl ausgewertet. +* Leere HTML-Strukturen, unsichtbare Zeichen und Leerzeichen sind keine Instruktion; + unterstützte Bilder und Formeln sind eine Instruktion. Allgemeine Instruktionen + allein machen keinen Code manuell. +* Code 0 und Missing 0 bleiben erhalten. Leere Exporte liefern `[]` bzw. ein gültiges + DOCX. Fehlerhafte Schemen melden die betroffene Aufgabe. +* Aufgaben werden nach Schlüssel, Variablen nach Alias/ID sortiert; Codefolge bleibt. +* BASE_NO_VALUE bleibt ausgeschlossen; abgeleitete Variablen folgen der Option. +* Itembeziehungen sind explizite `{id, variableId}`-Paare. Der Anwendungsadapter + normalisiert Quell-IDs auf die im Dokument verwendeten Aliase. +* Die Kennzeichnung lautet IQB Codebook. Studio-Formatierung, Tabellen, Seitenfelder, + eingebettete Bilder und Formelkonvertierung bleiben erhalten. Klartextinstruktionen + und Leerzeichen an Formatgrenzen werden nicht mehr verschluckt. + +## Paket und Laufzeit + +`npm run build_cc` erzeugt Angular-ESM sowie einen zusätzlichen CommonJS-Einstieg +für Server. Der Generator braucht weder Angular noch DOM/Browser-Globals. +Der CommonJS-Build integriert den ausschließlich als ESM gelieferten mathml2omml- +Konverter. Dessen Quellcode bleibt über Source Maps und das npm-Original verfügbar. + +Die Regeltextkonvertierung ist intern auf @iqb/responses 5.2.2 festgelegt. +Das ist ausdrücklich kein Upgrade der Kodierbox-Kodierengine: deren responses +5.2.0 und response-Spezifikation 1.5.1 bleiben unverändert. Die Bibliothek akzeptiert +response-Spezifikation 1.5.1 sowie 2.x als Peer. + +## Lokale Integration und Veröffentlichung + +Die beiden Integrationen referenzieren dieselbe gepackte Datei unter +`vendor/iqb-ngx-coding-components-4.1.0.tgz`. Das ist ein lokal prüfbarer Release- +Kandidat, noch keine veröffentlichte npm-Version. Zum Release wird genau der +abgenommene Bibliotheksstand veröffentlicht; danach werden die Host-Abhängigkeiten +auf die Registry-Version umgestellt und Locks neu geprüft. Die lokalen Tarballs +können dann entfernt werden. Beide Anwendungen gemeinsam abnehmen und ausrollen. +Rollback: vorherige Bibliotheks-/Anwendungsversion; keine Datenmigration. + +## Prüfungen + +* `npm run test:codebook`: Node-Vertragstests für alle Filterkombinationen, Daten, + JSON, DOCX-Struktur, Bilder, Formeln, Itembeziehungen und CJS/ESM-Gleichheit. +* `npm run test:cc`: Angular-Tests einschließlich Auswahl/Suche/Sperrzustände. +* Beide Hosts testen ihre echten Adapter und bestehenden Formelregressionen. +* Studio-Frontend prüft direkten Export, Ladezustände, Fehlermeldung und Speichersperre. +* Kodierbox prüft Scope-Filter, wiederholte Starts, langsame Statusabfragen und Schließen. +* Frontend- und Backend-Prüfungen auf dem Entwicklungsrechner nacheinander ausführen. +* Vor Ausrollen bleiben visuelle Mehrseiten-DOCX-Abnahme und Host-Browserabläufe + einschließlich Netzwerkprüfung verpflichtend. From d8964b49002c8fb44f45ff8f98d9fcee58ff382e Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:06:56 +0200 Subject: [PATCH 03/36] fix: keep host-specific filters outside shared codebook options --- .../codebook-export/codebook-export.component.ts | 7 ++++++- .../codebook-export.component.spec.ts | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/projects/ngx-coding-components/codebook-export/codebook-export.component.ts b/projects/ngx-coding-components/codebook-export/codebook-export.component.ts index aae6de2..73db93a 100644 --- a/projects/ngx-coding-components/codebook-export/codebook-export.component.ts +++ b/projects/ngx-coding-components/codebook-export/codebook-export.component.ts @@ -61,7 +61,12 @@ export class CodebookExportComponent implements OnChanges, AfterViewInit { contentOptions = { ...DEFAULT_CODEBOOK_OPTIONS }; ngOnChanges(changes: SimpleChanges): void { - if (changes['defaultContentOptions']) this.contentOptions = { ...DEFAULT_CODEBOOK_OPTIONS, ...this.defaultContentOptions }; + if (changes['defaultContentOptions']) { + // Only shared options cross this boundary; host-specific filters stay with the host. + this.contentOptions = Object.fromEntries(Object.entries(DEFAULT_CODEBOOK_OPTIONS).map(([key, value]) => ( + [key, this.defaultContentOptions[key as keyof CodeBookContentSetting] ?? value] + ))) as unknown as CodeBookContentSetting; + } if (changes['availableUnits']) { this.dataSource.data = this.availableUnits; this.dataSource.filterPredicate = (unit, filter) => [unit.key, unit.unitName, unit.groupName || ''] diff --git a/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts b/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts index edba782..74cb6f6 100644 --- a/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts +++ b/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts @@ -49,6 +49,20 @@ describe('Studio codebook selection contract', () => { expect(config.contentOptions.missingsProfile).toBe('Profil'); }); + it('does not copy or emit host-specific options', () => { + const hostOptions = { showScore: false, trainingRequirement: 'all', jobDefinitionId: 10 }; + component.defaultContentOptions = hostOptions; + component.ngOnChanges({ defaultContentOptions: new SimpleChange(null, hostOptions, true) }); + hostOptions.trainingRequirement = 'required'; + component.toggle(component.availableUnits[0]); + const emit = spyOn(component.exportRequested, 'emit'); + component.exportCodingBook(); + const options = emit.calls.mostRecent().args[0]!.contentOptions; + expect(options.showScore).toBeFalse(); + expect(Object.keys(options)).not.toContain('trainingRequirement'); + expect(Object.keys(options)).not.toContain('jobDefinitionId'); + }); + it('blocks export for empty selection, loading, busy and unsaved changes', () => { const emit = spyOn(component.exportRequested, 'emit'); component.exportCodingBook(); component.toggle(component.availableUnits[0]); From 853c733b540f1b442dedc7ff0210211076f4c80c Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:06:56 +0200 Subject: [PATCH 04/36] fix: exclude invisible-only codebook instructions --- .../codebook-generator/manual-instruction.ts | 2 +- scripts/codebook-contract.test.mjs | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/projects/ngx-coding-components/codebook-generator/manual-instruction.ts b/projects/ngx-coding-components/codebook-generator/manual-instruction.ts index 0a42602..460633a 100644 --- a/projects/ngx-coding-components/codebook-generator/manual-instruction.ts +++ b/projects/ngx-coding-components/codebook-generator/manual-instruction.ts @@ -5,7 +5,7 @@ export function hasCodebookManualInstruction(instruction?: string | null): boole if (!instruction) return false; const $ = load(instruction); $('script,style').remove(); - const text = $.root().text().replace(/[\s\u200b-\u200d\ufeff]/g, ''); + const text = $.root().text().replace(/[\s\p{Default_Ignorable_Code_Point}]/gu, ''); if (text) return true; return $('img').toArray().some(element => /^data:image\/(png|jpeg|jpg|gif|bmp);base64,\S+$/i.test($(element).attr('src') || '')) || $('span.iqb-math-formula').toArray().some(element => !!$(element).attr('data-latex')?.trim()); diff --git a/scripts/codebook-contract.test.mjs b/scripts/codebook-contract.test.mjs index 302756c..a384e79 100644 --- a/scripts/codebook-contract.test.mjs +++ b/scripts/codebook-contract.test.mjs @@ -24,7 +24,7 @@ for (const manual of [false, true]) for (const closed of [false, true]) for (con } test('manual rich text handles empty HTML, invisible text, image and formula-only content', () => { - for (const empty of ['', ' ', '

 ​

', '


', '']) assert.equal(hasCodebookManualInstruction(empty), false, empty); + for (const empty of ['', ' ', '

 ​

', '


', '', '

⁠

', '

­

', '

️

']) assert.equal(hasCodebookManualInstruction(empty), false, empty); for (const nonempty of ['Text', '

<

', '', '', '[[iqb-math:x^2]]']) assert.equal(hasCodebookManualInstruction(nonempty), true, nonempty); }); @@ -135,3 +135,9 @@ test('Studio reference preserves document, styles, numbering, settings and page assert.equal(actual, expected, file); } }); + +test('invisible-only instructions do not include variables in manual exports', () => { + for (const instruction of ['

⁠

', '

­

', '

️

']) { + assert.deepEqual(data([variable('EMPTY', [code(1, instruction)])], { hasOnlyManualCoding: true }), []); + } +}); From 21537078b13d474397ccc06482221d7af7209528 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:26:35 +0200 Subject: [PATCH 05/36] fix: preserve nested codebook formatting and word spacing --- .../codebook-docx-generator.ts | 206 +++--------------- scripts/codebook-contract.test.mjs | 17 ++ 2 files changed, 51 insertions(+), 172 deletions(-) diff --git a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts index 9e570f7..3f58a02 100644 --- a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts +++ b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts @@ -15,7 +15,8 @@ import { PageNumber, ITableCellBorders, ImportedXmlComponent, - ParagraphChild + ParagraphChild, + IRunOptions } from 'docx'; import { @@ -393,181 +394,42 @@ export class CodebookDocxGenerator { children: ParagraphChild[], colorParsed: string, backgroundColor: string, - size: string + size: string, + formatting: Pick = {} ): void { + const runStyle = { + ...formatting, + color: colorParsed, + shading: { fill: backgroundColor }, + size: CodebookDocxGenerator.getFontSize(size) + }; nodes.forEach(node => { if (node.type === 'text') { - if ('data' in node && node.data && node.data.trim()) { - children.push( - new TextRun({ - text: node.data.replace(/\s+/g, ' '), - color: colorParsed, - shading: { - fill: backgroundColor - }, - size: CodebookDocxGenerator.getFontSize(size) - }) - ); - } - } else if (node.type === 'tag') { - const element = node as Element; - const tagName = element.name.toLowerCase(); - - if ( - tagName === 'span' && - element.attribs?.['class']?.includes('iqb-math-formula') - ) { - const rawLatex = element.attribs?.['data-latex'] || ''; - const latex = CodebookDocxGenerator.decodeLatex(rawLatex).trim(); - if (latex) { - const ommlComponent = CodebookDocxGenerator.latexToOmml(latex); - if (ommlComponent) { - children.push(ommlComponent); - } else { - children.push( - new TextRun({ - text: latex, - color: colorParsed, - shading: { - fill: backgroundColor - }, - size: CodebookDocxGenerator.getFontSize(size) - }) - ); - } - } - return; - } - - if (tagName === 'strong' || tagName === 'b') { - if (element.children) { - element.children.forEach(child => { - if (child.type === 'text' && child.data) { - children.push( - new TextRun({ - text: child.data.replace(/\s+/g, ' '), - bold: true, - color: colorParsed, - shading: { - fill: backgroundColor - }, - size: CodebookDocxGenerator.getFontSize(size) - }) - ); - } - }); - } - } else if (tagName === 'em' || tagName === 'i') { - if (element.children) { - element.children.forEach(child => { - if (child.type === 'text' && child.data) { - children.push( - new TextRun({ - text: child.data.replace(/\s+/g, ' '), - italics: true, - color: colorParsed, - shading: { - fill: backgroundColor - }, - size: CodebookDocxGenerator.getFontSize(size) - }) - ); - } - }); - } - } else if (tagName === 'u') { - if (element.children) { - element.children.forEach(child => { - if (child.type === 'text' && child.data) { - children.push( - new TextRun({ - text: child.data.replace(/\s+/g, ' '), - underline: {}, - color: colorParsed, - shading: { - fill: backgroundColor - }, - size: CodebookDocxGenerator.getFontSize(size) - }) - ); - } - }); - } - } else if (tagName === 's') { - if (element.children) { - element.children.forEach(child => { - if (child.type === 'text' && child.data) { - children.push( - new TextRun({ - text: child.data.replace(/\s+/g, ' '), - strike: true, - color: colorParsed, - shading: { - fill: backgroundColor - }, - size: CodebookDocxGenerator.getFontSize(size) - }) - ); - } - }); - } - } else if (tagName === 'sub') { - if (element.children) { - element.children.forEach(child => { - if (child.type === 'text' && child.data) { - children.push( - new TextRun({ - text: child.data.replace(/\s+/g, ' '), - subScript: true, - color: colorParsed, - shading: { - fill: backgroundColor - }, - size: CodebookDocxGenerator.getFontSize(size) - }) - ); - } - }); - } - } else if (tagName === 'sup') { - if (element.children) { - element.children.forEach(child => { - if (child.type === 'text' && child.data) { - children.push( - new TextRun({ - text: child.data.replace(/\s+/g, ' '), - superScript: true, - color: colorParsed, - shading: { - fill: backgroundColor - }, - size: CodebookDocxGenerator.getFontSize(size) - }) - ); - } - }); - } - } else if (tagName === 'br') { - children.push( - new TextRun({ - break: 1, - color: colorParsed, - shading: { - fill: backgroundColor - }, - size: CodebookDocxGenerator.getFontSize(size) - }) - ); - } else if (element.children && element.children.length > 0) { - CodebookDocxGenerator.processInlineElements( - element.children, - children, - colorParsed, - backgroundColor, - size - ); - } + if (node.data) children.push(new TextRun({ text: node.data.replace(/\s+/g, ' '), ...runStyle })); + return; } + if (node.type !== 'tag') return; + const element = node as Element; + const tagName = element.name.toLowerCase(); + if (tagName === 'span' && element.attribs?.['class']?.split(/\s+/).includes('iqb-math-formula')) { + const latex = CodebookDocxGenerator.decodeLatex(element.attribs?.['data-latex'] || '').trim(); + if (latex) children.push(CodebookDocxGenerator.latexToOmml(latex) || new TextRun({ text: latex, ...runStyle })); + return; + } + if (tagName === 'br') { + children.push(new TextRun({ break: 1, ...runStyle })); + return; + } + const nestedFormatting = { ...formatting }; + if (tagName === 'strong' || tagName === 'b') nestedFormatting.bold = true; + if (tagName === 'em' || tagName === 'i') nestedFormatting.italics = true; + if (tagName === 'u') nestedFormatting.underline = {}; + if (tagName === 's' || tagName === 'strike') nestedFormatting.strike = true; + if (tagName === 'sub') { nestedFormatting.subScript = true; nestedFormatting.superScript = false; } + if (tagName === 'sup') { nestedFormatting.superScript = true; nestedFormatting.subScript = false; } + CodebookDocxGenerator.processInlineElements( + element.children, children, colorParsed, backgroundColor, size, nestedFormatting + ); }); } diff --git a/scripts/codebook-contract.test.mjs b/scripts/codebook-contract.test.mjs index a384e79..4e11bfb 100644 --- a/scripts/codebook-contract.test.mjs +++ b/scripts/codebook-contract.test.mjs @@ -141,3 +141,20 @@ test('invisible-only instructions do not include variables in manual exports', ( assert.deepEqual(data([variable('EMPTY', [code(1, instruction)])], { hasOnlyManualCoding: true }), []); } }); + +test('DOCX retains nested marks and spaces between differently formatted runs', async () => { + const { xml } = await document('

BEWERTEN Text unten oben

'); + const $ = (await import('cheerio')).load(xml, { xml: true }); + const runs = $('w\\:r').toArray(); + const findRun = text => runs.find(run => $(run).find('w\\:t').text() === text); + assert.equal($(findRun('BEWERTEN')).find('w\\:b').length, 1); + assert.equal($(findRun('BEWERTEN')).find('w\\:i').length, 1); + assert.equal($(findRun('Text')).find('w\\:u').length, 1); + assert.equal($(findRun('Text')).find('w\\:strike').length, 1); + assert.equal($(findRun('unten')).find('w\\:vertAlign').attr('w:val'), 'subscript'); + assert.equal($(findRun('oben')).find('w\\:vertAlign').attr('w:val'), 'superscript'); + assert.match($('w\\:t').toArray().map(element => $(element).text()).join(''), /BEWERTEN Text unten oben/); + const spaced = await document('

vor nach

'); + const text = (await import('cheerio')).load(spaced.xml, { xml: true }); + assert.match(text('w\\:t').toArray().map(element => text(element).text()).join(''), /vor nach/); +}); From d0179ae816217944f22097c07bc207d98c9f1143 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:26:35 +0200 Subject: [PATCH 06/36] fix: lock codebook selection while loading and report invalidated IDs --- .../codebook-export.component.html | 2 +- .../codebook-export.component.ts | 10 +++++++--- .../codebook-export.component.spec.ts | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/projects/ngx-coding-components/codebook-export/codebook-export.component.html b/projects/ngx-coding-components/codebook-export/codebook-export.component.html index 5b75947..c6e7f3c 100644 --- a/projects/ngx-coding-components/codebook-export/codebook-export.component.html +++ b/projects/ngx-coding-components/codebook-export/codebook-export.component.html @@ -26,7 +26,7 @@

{{ 'codebook.workspace.export-coding-book' | translate }} - diff --git a/projects/ngx-coding-components/codebook-export/codebook-export.component.ts b/projects/ngx-coding-components/codebook-export/codebook-export.component.ts index 73db93a..493b66e 100644 --- a/projects/ngx-coding-components/codebook-export/codebook-export.component.ts +++ b/projects/ngx-coding-components/codebook-export/codebook-export.component.ts @@ -78,7 +78,11 @@ export class CodebookExportComponent implements OnChanges, AfterViewInit { } if (changes['selectedUnitIds']) this.selection = [...this.selectedUnitIds]; const validIds = new Set(this.availableUnits.filter(unit => !unit.disabled).map(unit => unit.unitId)); - this.selection = this.selection.filter(id => validIds.has(id)); + const validSelection = this.selection.filter(id => validIds.has(id)); + if (validSelection.length !== this.selection.length) { + this.selection = validSelection; + this.selectionChanged.emit([...this.selection]); + } } ngAfterViewInit(): void { this.dataSource.sort = this.sort; } @@ -95,13 +99,13 @@ export class CodebookExportComponent implements OnChanges, AfterViewInit { applyFilter(): void { this.dataSource.filter = this.filter.trim().toLowerCase(); } toggle(unit: UnitSelectionItem): void { - if (unit.disabled || this.busy) return; + if (unit.disabled || this.loading || this.busy) return; this.selection = this.selection.includes(unit.unitId) ? this.selection.filter(id => id !== unit.unitId) : [...this.selection, unit.unitId]; this.selectionChanged.emit([...this.selection]); } toggleAll(): void { - if (this.busy) return; + if (this.loading || this.busy) return; this.selection = this.allSelected ? [] : this.availableUnits.filter(unit => !unit.disabled).map(unit => unit.unitId); this.selectionChanged.emit([...this.selection]); } diff --git a/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts b/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts index 74cb6f6..27cc351 100644 --- a/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts +++ b/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts @@ -63,6 +63,23 @@ describe('Studio codebook selection contract', () => { expect(Object.keys(options)).not.toContain('jobDefinitionId'); }); + it('blocks individual and complete selection while loading', () => { + component.loading = true; + const emit = spyOn(component.selectionChanged, 'emit'); + component.toggle(component.availableUnits[0]); component.toggleAll(); + expect(component.selection).toEqual([]); + expect(emit).not.toHaveBeenCalled(); + }); + + it('notifies the host when refreshed units invalidate its selection', () => { + component.toggleAll(); + const emit = spyOn(component.selectionChanged, 'emit'); + component.availableUnits = [component.availableUnits[0]]; + component.ngOnChanges({ availableUnits: new SimpleChange(null, component.availableUnits, false) }); + expect(component.selection).toEqual([1]); + expect(emit).toHaveBeenCalledOnceWith([1]); + }); + it('blocks export for empty selection, loading, busy and unsaved changes', () => { const emit = spyOn(component.exportRequested, 'emit'); component.exportCodingBook(); component.toggle(component.availableUnits[0]); From fc71cea7085fb35f22877c351dc996604905d29e Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:34:24 +0200 Subject: [PATCH 07/36] feat: share codebook training filter and Studio group column --- docs/shared-codebook.md | 14 ++++++++++++-- .../codebook-export.component.html | 12 +++++++++++- .../codebook-export/codebook-export.component.ts | 9 +++++++-- .../codebook-generator/codebook-generator.ts | 3 +++ .../codebook-models/public-api.ts | 4 ++++ .../codebook-export.component.spec.ts | 16 +++++++++++++--- scripts/codebook-contract.test.mjs | 10 ++++++++++ 7 files changed, 60 insertions(+), 8 deletions(-) diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md index 40c54ac..23f62ad 100644 --- a/docs/shared-codebook.md +++ b/docs/shared-codebook.md @@ -9,7 +9,9 @@ keinen Jobvertrag, keine HTTP-Aufrufe und keinen Download-Dienst. Studio lädt Aufgaben und Missingprofile, übergibt die aktive Aufgabe und seine Speichersperre und verarbeitet `exportRequested` mit seinem direkten API-Aufruf. -Die Kodierbox ergänzt ihre eigenen Filter über `[codebookFilters]` und zeigt ihre +Schulungsbedarf ist eine gemeinsame Auswahl und wird in beiden Anwendungen durch +denselben Generator anhand von CODER_TRAINING_REQUIRED gefiltert. +Die Kodierbox ergänzt ihren Jobdefinitionsfilter über `[codebookFilters]` und zeigt ihre Jobzustände über `[codebookStatus]`. Das sind Projektionsflächen ohne Jobsemantik. `loading` und `busy` sind allgemeine Sperrzustände. Nur die Kodierbox startet Jobs, fragt Status ab und lädt Ergebnisse. Beim Schließen endet die Statusabfrage; @@ -20,6 +22,11 @@ verwenden dieselben Studio-Beschriftungen und Tooltips. `selectionChanged`, `exportRequested` und `cancel` sind die einzigen Ausgaben. Der Exportwunsch enthält Aufgaben-IDs, fachliche Optionen und Missingprofil-ID. Die Profile tragen eine numerische ID und eine Beschriftung; 0 bedeutet kein Profil. +`showGroupColumn` ist im Studio aktiv, in Kodierbox deaktiviert. +Während `loading` und `busy` ist jede Auswahl gesperrt. Verliert eine Aufgabe ihre +Verfügbarkeit, meldet selectionChanged auch die bereinigte Auswahl zurück. +Variablenbündel bleiben als bestehender Kodierbox-API-Vertrag erhalten, werden +aber im Codebook-Dialog nicht mehr angeboten (Issue #176). ## Bewusste fachliche Korrekturen @@ -34,12 +41,15 @@ numerische ID und eine Beschriftung; 0 bedeutet kein Profil. * Code 0 und Missing 0 bleiben erhalten. Leere Exporte liefern `[]` bzw. ein gültiges DOCX. Fehlerhafte Schemen melden die betroffene Aufgabe. * Aufgaben werden nach Schlüssel, Variablen nach Alias/ID sortiert; Codefolge bleibt. +* Schulungsbedarf: alle, nur mit oder nur ohne CODER_TRAINING_REQUIRED; kombiniert + mit der gemeinsamen Variablen-/Codeauswahl. * BASE_NO_VALUE bleibt ausgeschlossen; abgeleitete Variablen folgen der Option. * Itembeziehungen sind explizite `{id, variableId}`-Paare. Der Anwendungsadapter normalisiert Quell-IDs auf die im Dokument verwendeten Aliase. * Die Kennzeichnung lautet IQB Codebook. Studio-Formatierung, Tabellen, Seitenfelder, eingebettete Bilder und Formelkonvertierung bleiben erhalten. Klartextinstruktionen - und Leerzeichen an Formatgrenzen werden nicht mehr verschluckt. + und Leerzeichen an Formatgrenzen werden nicht mehr verschluckt. Verschachtelte + Textmarkierungen bleiben kombiniert erhalten. ## Paket und Laufzeit diff --git a/projects/ngx-coding-components/codebook-export/codebook-export.component.html b/projects/ngx-coding-components/codebook-export/codebook-export.component.html index c6e7f3c..1d55539 100644 --- a/projects/ngx-coding-components/codebook-export/codebook-export.component.html +++ b/projects/ngx-coding-components/codebook-export/codebook-export.component.html @@ -51,6 +51,17 @@

{{ 'codebook.workspace.export-coding-book' | translate }}
+ +
+ + {{ 'codebook.coding.training-requirement-filter' | translate }} + + {{ 'codebook.coding.training-requirement-all' | translate }} + {{ 'codebook.coding.training-requirement-required' | translate }} + {{ 'codebook.coding.training-requirement-not-required' | translate }} + + +

{{'codebook.coding.codebook-content' | translate}}

@@ -126,7 +137,6 @@

{{'codebook.coding.export-format' | translate}}

- @if(workspaceChanges) { {{'codebook.coding.error-save-changes' | translate}} } diff --git a/projects/ngx-coding-components/codebook-export/codebook-export.component.ts b/projects/ngx-coding-components/codebook-export/codebook-export.component.ts index 493b66e..ad70524 100644 --- a/projects/ngx-coding-components/codebook-export/codebook-export.component.ts +++ b/projects/ngx-coding-components/codebook-export/codebook-export.component.ts @@ -27,7 +27,8 @@ export const DEFAULT_CODEBOOK_OPTIONS: CodeBookContentSetting = { hasClosedVars: true, codeLabelToUpper: true, showScore: true, - hideItemVarRelation: true + hideItemVarRelation: true, + trainingRequirement: 'all' }; /** Studio's export form. Loading data, downloading and job handling belong to the host. */ @@ -46,6 +47,7 @@ export class CodebookExportComponent implements OnChanges, AfterViewInit { @Input() selectedUnitIds: number[] = []; @Input() selectedMissingsProfileId = 0; @Input() defaultContentOptions: Partial = {}; + @Input() showGroupColumn = true; @Input() loading = false; @Input() busy = false; @Input() workspaceChanges = false; @@ -61,6 +63,9 @@ export class CodebookExportComponent implements OnChanges, AfterViewInit { contentOptions = { ...DEFAULT_CODEBOOK_OPTIONS }; ngOnChanges(changes: SimpleChanges): void { + if (changes['showGroupColumn']) { + this.columns = this.showGroupColumn ? ['select', 'key', 'name', 'group'] : ['select', 'key', 'name']; + } if (changes['defaultContentOptions']) { // Only shared options cross this boundary; host-specific filters stay with the host. this.contentOptions = Object.fromEntries(Object.entries(DEFAULT_CODEBOOK_OPTIONS).map(([key, value]) => ( @@ -69,7 +74,7 @@ export class CodebookExportComponent implements OnChanges, AfterViewInit { } if (changes['availableUnits']) { this.dataSource.data = this.availableUnits; - this.dataSource.filterPredicate = (unit, filter) => [unit.key, unit.unitName, unit.groupName || ''] + this.dataSource.filterPredicate = (unit, filter) => [unit.key, unit.unitName, this.showGroupColumn ? unit.groupName || '' : ''] .some(value => value.toLowerCase().includes(filter)); this.dataSource.sortingDataAccessor = (unit, column) => ({ key: unit.key, name: unit.unitName, group: unit.groupName || '' }[column] || ''); } diff --git a/projects/ngx-coding-components/codebook-generator/codebook-generator.ts b/projects/ngx-coding-components/codebook-generator/codebook-generator.ts index 9005e3e..cfa4ab9 100644 --- a/projects/ngx-coding-components/codebook-generator/codebook-generator.ts +++ b/projects/ngx-coding-components/codebook-generator/codebook-generator.ts @@ -53,6 +53,9 @@ export class CodebookGenerator { } private static getVariable(variable: VariableCodingData, options: CodeBookContentSetting): BookVariable | null { + const trainingRequired = variable.processing?.includes('CODER_TRAINING_REQUIRED') ?? false; + if (options.trainingRequirement === 'required' && !trainingRequired) return null; + if (options.trainingRequirement === 'not-required' && trainingRequired) return null; if (variable.sourceType === 'BASE_NO_VALUE' || (variable.sourceType !== 'BASE' && !options.hasDerivedVars)) return null; const codes = (variable.codes || []).filter(code => code.id !== undefined && code.id !== null); const manual = codes.some(code => this.isManual(code)); diff --git a/projects/ngx-coding-components/codebook-models/public-api.ts b/projects/ngx-coding-components/codebook-models/public-api.ts index 1b21f73..9f1bcc1 100644 --- a/projects/ngx-coding-components/codebook-models/public-api.ts +++ b/projects/ngx-coding-components/codebook-models/public-api.ts @@ -11,6 +11,8 @@ export interface ItemMetadata { /** * Settings for codebook content generation */ +export type CodebookTrainingRequirementFilter = 'all' | 'required' | 'not-required'; + export interface CodeBookContentSetting { /** Export format (docx or json) */ exportFormat: 'json' | 'docx'; @@ -32,6 +34,8 @@ export interface CodeBookContentSetting { showScore: boolean; /** Hide item-variable relation */ hideItemVarRelation: boolean; + /** Filter variables by increased coder training requirement; omission means all. */ + trainingRequirement?: CodebookTrainingRequirementFilter; } /** diff --git a/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts b/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts index 27cc351..e4ea964 100644 --- a/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts +++ b/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts @@ -50,19 +50,29 @@ describe('Studio codebook selection contract', () => { }); it('does not copy or emit host-specific options', () => { - const hostOptions = { showScore: false, trainingRequirement: 'all', jobDefinitionId: 10 }; + const hostOptions = { showScore: false, jobDefinitionId: 10, variableBundleIds: [7] }; component.defaultContentOptions = hostOptions; component.ngOnChanges({ defaultContentOptions: new SimpleChange(null, hostOptions, true) }); - hostOptions.trainingRequirement = 'required'; component.toggle(component.availableUnits[0]); const emit = spyOn(component.exportRequested, 'emit'); component.exportCodingBook(); const options = emit.calls.mostRecent().args[0]!.contentOptions; expect(options.showScore).toBeFalse(); - expect(Object.keys(options)).not.toContain('trainingRequirement'); + expect(Object.keys(options)).not.toContain('variableBundleIds'); expect(Object.keys(options)).not.toContain('jobDefinitionId'); }); + it('exports the shared training filter and supports Studio-only groups', () => { + component.contentOptions.trainingRequirement = 'required'; + component.showGroupColumn = false; + component.ngOnChanges({ showGroupColumn: new SimpleChange(true, false, false) }); + expect(component.columns).toEqual(['select', 'key', 'name']); + component.toggle(component.availableUnits[0]); + const emit = spyOn(component.exportRequested, 'emit'); + component.exportCodingBook(); + expect(emit.calls.mostRecent().args[0]!.contentOptions.trainingRequirement).toBe('required'); + }); + it('blocks individual and complete selection while loading', () => { component.loading = true; const emit = spyOn(component.selectionChanged, 'emit'); diff --git a/scripts/codebook-contract.test.mjs b/scripts/codebook-contract.test.mjs index 4e11bfb..e41dd99 100644 --- a/scripts/codebook-contract.test.mjs +++ b/scripts/codebook-contract.test.mjs @@ -158,3 +158,13 @@ test('DOCX retains nested marks and spaces between differently formatted runs', const text = (await import('cheerio')).load(spaced.xml, { xml: true }); assert.match(text('w\\:t').toArray().map(element => text(element).text()).join(''), /vor nach/); }); + +test('shared training filters combine with manual/closed and derived-variable selection', () => { + const vars = [variable('TRAIN', [code(0, 'Bewerten')], { processing: ['CODER_TRAINING_REQUIRED'] }), + variable('OTHER', [code(1, 'Bewerten')]), variable('CLOSED', [code(0, '', 'RESIDUAL_AUTO')], { processing: ['CODER_TRAINING_REQUIRED'] }), + variable('DERIVED', [code(1, 'Bewerten')], { sourceType: 'COPY_VALUE', processing: ['CODER_TRAINING_REQUIRED'] })]; + assert.deepEqual(data(vars, { trainingRequirement: 'required', hasOnlyManualCoding: true, hasDerivedVars: false }).map(v => v.id), ['TRAIN']); + assert.deepEqual(data(vars, { trainingRequirement: 'not-required', hasOnlyManualCoding: true }).map(v => v.id), ['OTHER']); + assert.deepEqual(data(vars, { trainingRequirement: 'required', hasClosedVars: true }).map(v => v.id), ['CLOSED']); + assert.deepEqual(data(vars, { trainingRequirement: 'all' }).map(v => v.id), data(vars).map(v => v.id)); +}); From d998221924749919832c9c00810d0e5518dc064f Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:42:07 +0200 Subject: [PATCH 08/36] fix: expose accessible codebook checkbox labels --- .../codebook-export.component.html | 4 ++-- .../codebook-export.component.spec.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/projects/ngx-coding-components/codebook-export/codebook-export.component.html b/projects/ngx-coding-components/codebook-export/codebook-export.component.html index 1d55539..3a9c1bb 100644 --- a/projects/ngx-coding-components/codebook-export/codebook-export.component.html +++ b/projects/ngx-coding-components/codebook-export/codebook-export.component.html @@ -22,12 +22,12 @@

{{ 'codebook.workspace.export-coding-book' | translate }} + [attr.data-cy]="'workspace-select-unit-list-checkbox-' + unit.key" [aria-label]="unit.key" (change)="toggle(unit)"> diff --git a/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts b/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts index e4ea964..d4a5019 100644 --- a/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts +++ b/projects/ngx-coding-components/src/lib/codebook-export/codebook-export.component.spec.ts @@ -1,4 +1,6 @@ import { SimpleChange } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; import { CodebookExportComponent } from '../../../codebook-export/codebook-export.component'; describe('Studio codebook selection contract', () => { @@ -99,3 +101,18 @@ describe('Studio codebook selection contract', () => { expect(emit).not.toHaveBeenCalled(); }); }); + +describe('codebook checkbox accessibility', () => { + it('labels the actual checkbox inputs for assistive technology', async () => { + await TestBed.configureTestingModule({ + imports: [CodebookExportComponent, TranslateModule.forRoot()] + }).compileComponents(); + const fixture = TestBed.createComponent(CodebookExportComponent); + fixture.componentRef.setInput('availableUnits', [{ unitId: 1, key: 'TASK', unitName: 'Aufgabe' }]); + fixture.detectChanges(); + const inputs = fixture.nativeElement.querySelectorAll('input[type="checkbox"]'); + expect(inputs[0].getAttribute('aria-label')).toBe('codebook.coding.select-all-units'); + expect(inputs[1].getAttribute('aria-label')).toBe('TASK'); + fixture.destroy(); + }); +}); From 8217bb8d9a01d14a5fb140ead9cde0f927b54b0b Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:52:08 +0200 Subject: [PATCH 09/36] fix(codebook): preserve loose instruction text and list contents --- docs/shared-codebook.md | 4 +- .../codebook-docx-generator.ts | 26 +++++++++ scripts/codebook-contract.test.mjs | 55 +++++++++++++++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md index 23f62ad..6a8aac8 100644 --- a/docs/shared-codebook.md +++ b/docs/shared-codebook.md @@ -49,7 +49,9 @@ aber im Codebook-Dialog nicht mehr angeboten (Issue #176). * Die Kennzeichnung lautet IQB Codebook. Studio-Formatierung, Tabellen, Seitenfelder, eingebettete Bilder und Formelkonvertierung bleiben erhalten. Klartextinstruktionen und Leerzeichen an Formatgrenzen werden nicht mehr verschluckt. Verschachtelte - Textmarkierungen bleiben kombiniert erhalten. + Textmarkierungen bleiben kombiniert erhalten. Direkte Listentexte und Klartext + vor, zwischen und nach HTML-Absätzen werden in Quellreihenfolge übernommen; + bestehende Absatzinhalte, Bilder und Formeln erscheinen weiterhin genau einmal. ## Paket und Laufzeit diff --git a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts index 3f58a02..d9d6f92 100644 --- a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts +++ b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts @@ -677,6 +677,31 @@ export class CodebookDocxGenerator { return !!elem.parent && (elem.parent as Element).name === 'li'; } + /** Put loose text and inline marks into paragraphs without duplicating existing blocks. */ + private static wrapInlineParagraphs(cheerioAPI: cheerio.CheerioAPI, parent: AnyNode): void { + let inlineNodes: AnyNode[] = []; + const flush = () => { + if (inlineNodes.some(node => node.type === 'tag' || (node.type === 'text' && node.data.trim()))) { + cheerioAPI(inlineNodes).wrapAll('

'); + } + inlineNodes = []; + }; + cheerioAPI(parent).contents().toArray().forEach(node => { + if (node.type === 'tag' && ( + /^(p|h[1-4]|img|ul|ol|li|div|blockquote)$/.test(node.name) || + cheerioAPI(node).find('p,h1,h2,h3,h4,img,ul,ol').length > 0 + )) { + flush(); + if (!/^(p|h[1-4]|img)$/.test(node.name)) { + CodebookDocxGenerator.wrapInlineParagraphs(cheerioAPI, node); + } + } else if (node.type === 'text' || node.type === 'tag') { + inlineNodes.push(node); + } + }); + flush(); + } + private static htmlToDocx( html: string, contentSetting: CodeBookContentSetting @@ -688,6 +713,7 @@ export class CodebookDocxGenerator { null, false ); + CodebookDocxGenerator.wrapInlineParagraphs(cheerioAPI, cheerioAPI.root()[0]); const elements: Paragraph[] = []; cheerioAPI('p,h1,h2,h3,h4,img').each((i, elem) => { try { diff --git a/scripts/codebook-contract.test.mjs b/scripts/codebook-contract.test.mjs index e41dd99..5565f45 100644 --- a/scripts/codebook-contract.test.mjs +++ b/scripts/codebook-contract.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { createRequire } from 'node:module'; import JSZip from 'jszip'; +import { load } from 'cheerio'; import { CodebookGenerator, CodebookDocxGenerator, hasCodebookManualInstruction } from '../dist/ngx-coding-components/fesm2022/iqb-ngx-coding-components-codebook-generator.mjs'; const options = { exportFormat: 'json', missingsProfile: '', hasOnlyManualCoding: false, @@ -102,6 +103,60 @@ test('plain instructions and whitespace around formatted text are not lost', asy assert.match(xml, />vor nach $(paragraph).find('w\\:t').text()); +} + +for (const tag of ['ul', 'ol']) { + test(`DOCX retains direct ${tag} list text and existing paragraph list items exactly once`, async () => { + const { xml } = await document(`<${tag}>
  • Erstes Kriterium
  • Zweites Kriterium

  • `); + assert.deepEqual(descriptionParagraphs(xml), ['Erstes Kriterium', 'Zweites Kriterium']); + const $ = load(xml, { xml: true }); + assert.equal($('w\\:numPr').length, 2); + assert.equal($('w\\:r').filter((_, run) => $(run).find('w\\:t').text() === 'Kriterium').find('w\\:b').length, 1); + }); +} + +test('DOCX keeps loose text and formatting before, between and after blocks in source order', async () => { + const { xml } = await document('Vor fett

    Regel

    Zwischen kursiv

    Hinweis

    Danach'); + assert.deepEqual(descriptionParagraphs(xml), ['Vor fett', 'Regel', 'Zwischen kursiv', 'Hinweis', 'Danach']); + const $ = load(xml, { xml: true }); + assert.equal($('w\\:r').filter((_, run) => $(run).find('w\\:t').text() === 'fett').find('w\\:b').length, 1); + assert.equal($('w\\:r').filter((_, run) => $(run).find('w\\:t').text() === 'kursiv').find('w\\:i').length, 1); +}); + +test('DOCX keeps mixed and nested list contents without repeating paragraphs or images', async () => { + const image = 'iVBORw0KGgoAAAANSUhEUgAAACgAAAAUCAIAAABwJOjsAAAAJElEQVR4nGOQSDgwIIhh1OJRi0ctHrV41OJRi0ctHrV45FgMAHBlzy43YLJ+AAAAAElFTkSuQmCC'; + const { xml, zip } = await document(`
    • Vor

      Absatz

      Danach
      • Unterpunkt [[iqb-math:x^2]]
    `); + assert.deepEqual(descriptionParagraphs(xml), ['Vor', 'Absatz', 'Danach', 'Unterpunkt ', '']); + const $ = load(xml, { xml: true }); + assert.equal($('w\\:drawing').length, 1); + assert.equal($('m\\:oMath').length, 1); + assert.equal(Object.keys(zip.files).filter(name => name.startsWith('word/media/') && !zip.files[name].dir).length, 1); +}); + +test('complete DOCX exports keep list instructions and plain instructions after generated rules in ESM and CJS', async () => { + const require = createRequire(import.meta.url); + const commonjs = require('../dist/ngx-coding-components/cjs/codebook-generator.cjs'); + for (const [instruction, ruleSets, expected] of [ + ['
    • Erstes Kriterium
    • Zweites Kriterium
    ', [], ['Erstes Kriterium', 'Zweites Kriterium']], + ['Manuelle Instruktion', [{ rules: [{ method: 'MATCH', parameters: ['ABC'] }], ruleOperatorAnd: true }], ['ABC', 'Manuelle Instruktion']] + ]) { + const units = [unit([variable('V', [{ ...code(1, instruction), ruleSets }])])]; + let reference; + for (const generator of [CodebookGenerator, commonjs.CodebookGenerator]) { + const blob = await generator.generateCodebook(units, { ...options, exportFormat: 'docx', hasOnlyManualCoding: true, hasClosedVars: true }, []); + const zip = await JSZip.loadAsync(await blob.arrayBuffer()); + const xml = await zip.file('word/document.xml').async('string'); + assert.deepEqual(descriptionParagraphs(xml), expected); + if (reference) assert.equal(xml, reference); + reference = xml; + } + } +}); + test('escaped inequality formulas produce valid XML text in inline and block form', async () => { const { xml } = await document('

    Prefix [[iqb-math:a%3Cb]] suffix

    '); assert.match(xml, /a<b/); assert.doesNotMatch(xml, //); From fd83caed87f06f8b85910d34b6d1f36d26cab159 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:36:57 +0200 Subject: [PATCH 10/36] fix: preserve Studio layout for whitespace-only instructions --- .../codebook-generator/codebook-docx-generator.ts | 1 + scripts/codebook-contract.test.mjs | 14 ++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts index d9d6f92..964a75b 100644 --- a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts +++ b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts @@ -706,6 +706,7 @@ export class CodebookDocxGenerator { html: string, contentSetting: CodeBookContentSetting ) { + if (!html.trim()) return []; const normalizedHtml = CodebookDocxGenerator.normalizeMathTokens(html); const blockHtml = /<(p|h[1-4]|img|ul|ol)\b/i.test(normalizedHtml) ? normalizedHtml : `

    ${normalizedHtml}

    `; const cheerioAPI = cheerio.load( diff --git a/scripts/codebook-contract.test.mjs b/scripts/codebook-contract.test.mjs index 5565f45..876821d 100644 --- a/scripts/codebook-contract.test.mjs +++ b/scripts/codebook-contract.test.mjs @@ -103,6 +103,20 @@ test('plain instructions and whitespace around formatted text are not lost', asy assert.match(xml, />vor nach { + const units = instruction => [{ key: 'A', name: 'Aufgabe', items: [], missings: [], variables: [{ + id: 'V', label: 'Variable', sourceType: 'BASE', generalInstruction: instruction, + codes: [{ id: '1', label: 'Code', score: '1', description: '

    Bewerten

    ' }] + }] }]; + let expected; + for (const instruction of ['', ' ', '\t\r\n', '\u00a0']) { + const zip = await JSZip.loadAsync(await (await CodebookDocxGenerator.generateDocx(units(instruction), options)).arrayBuffer()); + const actual = await zip.file('word/document.xml').async('string'); + if (expected) assert.equal(actual, expected, JSON.stringify(instruction)); + expected = actual; + } +}); + function descriptionParagraphs(xml) { const $ = load(xml, { xml: true }); return $('w\\:tr').first().children('w\\:tc').last().find('w\\:p').toArray() From d8b8316b67fcd1265f377dcdb540ab85d8b0cbff Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:43:45 +0200 Subject: [PATCH 11/36] test: cover codebook option matrix against original Studio exports --- package.json | 2 +- scripts/capture-studio-codebook-reference.mjs | 52 + scripts/codebook-docx-test-utils.mjs | 34 + scripts/codebook-matrix.test.mjs | 90 + scripts/fixtures/codebook-synthetic.mjs | 102 + scripts/fixtures/studio-codebook/README.md | 29 + .../studio-codebook/synthetic-reference.json | 9301 +++++++++++++++++ 7 files changed, 9609 insertions(+), 1 deletion(-) create mode 100644 scripts/capture-studio-codebook-reference.mjs create mode 100644 scripts/codebook-docx-test-utils.mjs create mode 100644 scripts/codebook-matrix.test.mjs create mode 100644 scripts/fixtures/codebook-synthetic.mjs create mode 100644 scripts/fixtures/studio-codebook/synthetic-reference.json diff --git a/package.json b/package.json index e94cb50..9376d3c 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test:schemer:coverage": "ng test schemer --watch=false --browsers=ChromeHeadless --code-coverage", "test:coverage": "npm run test:cc:coverage && npm run test:schemer:coverage", "test:scheme": "node --test scripts/validate-coding-scheme.test.js", - "test:codebook": "node --test scripts/codebook-contract.test.mjs", + "test:codebook": "node --test scripts/codebook-contract.test.mjs scripts/codebook-matrix.test.mjs", "test:schemer:responsive": "playwright test --config playwright.schemer.config.cjs" }, "dependencies": { diff --git a/scripts/capture-studio-codebook-reference.mjs b/scripts/capture-studio-codebook-reference.mjs new file mode 100644 index 0000000..3c679f8 --- /dev/null +++ b/scripts/capture-studio-codebook-reference.mjs @@ -0,0 +1,52 @@ +// Explicit maintainer operation, never run by CI. Requires a Studio checkout with +// the immutable pre-harmonisation commit; generates only invented fixture data. +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import { createRequire, Module } from 'node:module'; +import { writeFile } from 'node:fs/promises'; +import { referenceCorpus, referenceOptions } from './fixtures/codebook-synthetic.mjs'; +import { docxParts, partHashes } from './codebook-docx-test-utils.mjs'; + +const studio = process.argv[2]; +if (!studio) throw new Error('Usage: node scripts/capture-studio-codebook-reference.mjs /path/to/studio-checkout'); +const ref = '2504265721a59c7c65425072951ce82ab16f0fb8'; +const require = createRequire(import.meta.url); +const ts = require('typescript'); +const source = path => execFileSync('git', ['show', `${ref}:${path}`], { cwd: studio, encoding: 'utf8' }); +const math = await import('mathml2omml'); +function compile(code, overrides = {}) { + const output = ts.transpileModule(code, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS, esModuleInterop: true } }).outputText; + const mod = new Module(import.meta.filename); + mod.filename = import.meta.filename; + mod.paths = Module._nodeModulePaths(new URL('..', import.meta.url).pathname); + mod.require = name => overrides[name] || require(name); + mod._compile(output, mod.filename); + return mod.exports; +} +const colors = compile(source('apps/api/src/app/utils/web-colors.ts')); +const renderer = compile(source('apps/api/src/app/classes/download-docx.class.ts'), { + mathml2omml: math, '../utils/web-colors': colors +}).DownloadDocx; +const dataSource = source('apps/api/src/app/classes/download-workspaces.class.ts'); +const ast = ts.createSourceFile('baseline.ts', dataSource, ts.ScriptTarget.Latest, true); +const cls = ast.statements.find(node => ts.isClassDeclaration(node) && node.name.text === 'DownloadWorkspacesClass'); +const names = new Set(['isClosed', 'isManual', 'getCodeInfo', 'getCodeInfoFromCodeAsText', 'getRulesDescription', + 'getBaseOrDerivedBookVariable', 'getCodes', 'getManualOrClosedCodedBookVariable', 'getBookVariable', + 'getCodeBookDataForUnit', 'getBookVariables', 'getSortedBookVariables']); +const methods = cls.members.filter(node => node.name && names.has(node.name.getText(ast))).map(node => node.getText(ast)); +assert.equal(methods.length, names.size); +const adapter = compile(`import { ToTextFactory } from '@iqb/responses'; import { CodingScheme } from '@iqbspecs/coding-scheme'; + export class DownloadWorkspacesClass { ${methods.join('\n')} }`).DownloadWorkspacesClass; +const cases = []; +for (const options of referenceOptions()) { + const data = referenceCorpus().map(unit => adapter.getCodeBookDataForUnit(unit, options, + [{ code: 99, label: 'Missing', description: 'Keine Antwort' }])); + const json = data.map(({ key, name, variables, missings }) => ({ key, name, variables, missings })); + const parts = await docxParts(await renderer.getDocXCodebook(data, options)); + cases.push({ options, json, docxParts: partHashes(parts) }); +} +const fixture = { source: `iqb-berlin/studio-lite@${ref}`, docx: '8.5.0', responses: '5.2.2', + normalization: ['footer date', 'IQB-Studio Codebook -> IQB Codebook', 'exclude docProps/core.xml timestamps', + 'random media filenames in first-reference order; image bytes and relationships preserved'], cases }; +await writeFile(new URL('./fixtures/studio-codebook/synthetic-reference.json', import.meta.url), `${JSON.stringify(fixture, null, 2)}\n`); +console.log(`Captured ${cases.length} original Studio JSON/DOCX cases from raw invented schemas.`); diff --git a/scripts/codebook-docx-test-utils.mjs b/scripts/codebook-docx-test-utils.mjs new file mode 100644 index 0000000..223b31c --- /dev/null +++ b/scripts/codebook-docx-test-utils.mjs @@ -0,0 +1,34 @@ +import { createHash } from 'node:crypto'; +import JSZip from 'jszip'; + +export async function docxParts(output) { + const zip = await JSZip.loadAsync(output instanceof Blob ? await output.arrayBuffer() : output); + // docx assigns random media filenames. Preserve each relationship and each image + // byte, replacing only that random name in first-reference order. + const mediaNames = new Map(); + for (const [name, file] of Object.entries(zip.files).sort(([a], [b]) => a.localeCompare(b))) { + if (!name.endsWith('.rels')) continue; + for (const match of (await file.async('string')).matchAll(/Target="media\/([^"]+)"/g)) { + if (!mediaNames.has(match[1])) { + const bytes = await zip.file(`word/media/${match[1]}`).async('nodebuffer'); + mediaNames.set(match[1], `image-${mediaNames.size}-${createHash('sha256').update(bytes).digest('hex')}.${match[1].split('.').pop()}`); + } + } + } + const result = {}; + for (const [name, file] of Object.entries(zip.files).sort(([a], [b]) => a.localeCompare(b))) { + if (file.dir || name === 'docProps/core.xml') continue; // creation/modification timestamps, not document layout + const stableName = name.startsWith('word/media/') ? `word/media/${mediaNames.get(name.slice(11)) || name.slice(11)}` : name; + result[stableName] = name.endsWith('.xml') || name.endsWith('.rels') ? + (await file.async('string')).replaceAll(new Date().toLocaleDateString(), '{{DATE}}') + .replaceAll('IQB-Studio Codebook', 'IQB Codebook') : + await file.async('nodebuffer'); + if (typeof result[stableName] === 'string') { + for (const [random, stable] of mediaNames) result[stableName] = result[stableName].replaceAll(random, stable); + } + } + return result; +} +export function partHashes(parts) { + return Object.fromEntries(Object.entries(parts).map(([name, content]) => [name, createHash('sha256').update(content).digest('hex')])); +} diff --git a/scripts/codebook-matrix.test.mjs b/scripts/codebook-matrix.test.mjs new file mode 100644 index 0000000..ade5dd3 --- /dev/null +++ b/scripts/codebook-matrix.test.mjs @@ -0,0 +1,90 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { readFile } from 'node:fs/promises'; +import { load } from 'cheerio'; +import { CodebookGenerator } from '../dist/ngx-coding-components/fesm2022/iqb-ngx-coding-components-codebook-generator.mjs'; +import { defaults, selectionCorpus, expectedSelection, optionMatrix, missingCases, referenceCorpus, referenceOptions } from './fixtures/codebook-synthetic.mjs'; +import { docxParts, partHashes } from './codebook-docx-test-utils.mjs'; + +const corpus = selectionCorpus(); +const compactRows = corpus.rows.filter(({ variable }) => variable.sourceType === 'BASE' || + (variable.id.endsWith('_MIXED') && ['SUM_SCORE', 'BASE_NO_VALUE'].includes(variable.sourceType))); +const compactUnits = [{ ...corpus.units[1], scheme: JSON.stringify({ version: '3.4', variableCodings: compactRows.map(r => r.variable).reverse() }) }]; +const projection = variables => variables.map(({ id, codes }) => ({ id, codes: codes.map(c => c.id) })); + +for (const options of optionMatrix()) { + test(`all options ${JSON.stringify(options)}`, async () => { + const expected = expectedSelection(corpus.rows, options); + const compactExpected = expectedSelection(compactRows, options); + for (const missings of missingCases) { + const json = JSON.parse(await (await CodebookGenerator.generateCodebook(corpus.units, { ...options, exportFormat: 'json' }, missings)).text()); + assert.deepEqual(json.map(unit => unit.key), ['A', 'Z']); + assert.deepEqual(projection(json[0].variables), expected); + assert.deepEqual(json[1].variables, []); + assert.deepEqual(json[0].missings, missings); + assert.equal('items' in json[0], false); + for (const variable of json[0].variables) { + assert.equal(!!variable.generalInstruction, options.hasGeneralInstructions); + for (const code of variable.codes) { + assert.equal('score' in code, options.showScore); + if (options.showScore && code.id === '0') assert.equal(code.score, '0'); + if (options.codeLabelToUpper) assert.equal(code.label, code.label.toUpperCase()); + } + } + const parts = await docxParts(await CodebookGenerator.generateCodebook(compactUnits, { ...options, exportFormat: 'docx' }, missings)); + const $ = load(parts['word/document.xml'], { xml: true }); + const headings = $('w\\:p').filter((_, p) => $(p).find('w\\:pStyle').attr('w:val') === 'Heading2'); + assert.deepEqual(headings.toArray().map(p => $(p).find('w\\:t').text().split(' ')[0]), compactExpected.map(v => v.id)); + const tables = $('w\\:tbl').toArray(); + assert.equal(tables.length, compactExpected.length); + tables.forEach((table, i) => { + const rows = $(table).children('w\\:tr').toArray(); + assert.deepEqual(rows.map(row => $(row).children('w\\:tc').first().text()), compactExpected[i].codes); + rows.forEach(row => assert.equal($(row).children('w\\:tc').length, options.showScore ? 4 : 3)); + assert.equal($(table).find('w\\:cantSplit').length, rows.length); + // Widths needed by Microsoft Word and the grid needed by Writer are both preserved. + assert.equal($(table).children('w\\:tblGrid').children().length, options.showScore ? 4 : 3); + assert.equal($(table).find('w\\:tcW').length, rows.length * (options.showScore ? 4 : 3)); + }); + const paragraphs = $('w\\:body').children('w\\:p').toArray().map(p => $(p).find('w\\:t').text()); + assert.equal(paragraphs.filter(t => t.startsWith('Item(s):')).length, options.hideItemVarRelation ? 0 : compactExpected.length); + assert.equal(paragraphs.includes('Allgemeiner Hinweis'), options.hasGeneralInstructions && compactExpected.some(v => v.id.endsWith('_GENERAL_ONLY'))); + assert.equal(paragraphs.some(t => /^\s+$/.test(t)), false, 'whitespace instructions must not change page layout'); + for (const missing of missings) assert.ok(paragraphs.includes(`${missing.code} ${missing.label}`)); + const formulaCount = compactExpected.filter(v => v.id.endsWith('_FORMULA')).length; + const imageCount = compactExpected.filter(v => v.id.endsWith('_IMAGE')).length; + assert.equal($('m\\:oMath').length, formulaCount); + assert.equal($('w\\:drawing').length, imageCount); + assert.doesNotMatch(parts['word/document.xml'], / { + for (const version of ['3.0', '3.3', '3.4']) { + const unit = { ...compactUnits[0], scheme: JSON.stringify({ ...JSON.parse(compactUnits[0].scheme), version }) }; + assert.deepEqual(projection(CodebookGenerator.getCodebookData([unit], defaults, [])[0].variables), expectedSelection(compactRows, defaults)); + } + for (const scheme of ['{', '{}', '{"variableCodings":null}', '{"variableCodings":[null]}']) { + await assert.rejects(CodebookGenerator.generateCodebook([{ ...compactUnits[0], key: 'BROKEN', scheme }], defaults, []), /BROKEN/); + } +}); + +test('raw synthetic schemas match original Studio JSON and complete DOCX parts for all formatting combinations', async () => { + const expected = JSON.parse(await readFile(new URL('./fixtures/studio-codebook/synthetic-reference.json', import.meta.url), 'utf8')); + let index = 0; + for (const options of referenceOptions()) { + const entry = expected.cases[index++]; + assert.deepEqual(entry.options, options); + const units = referenceCorpus(); + const missings = [{ code: 99, label: 'Missing', description: 'Keine Antwort' }]; + const json = JSON.parse(await (await CodebookGenerator.generateCodebook(units, { ...options, exportFormat: 'json' }, missings)).text()); + assert.deepEqual(json, entry.json, `original Studio raw-schema JSON: case ${index}`); + const parts = await docxParts(await CodebookGenerator.generateCodebook(units, options, missings)); + assert.deepEqual(partHashes(parts), entry.docxParts, `original Studio DOCX: case ${index}`); + } + assert.equal(index, 32); +}); diff --git a/scripts/fixtures/codebook-synthetic.mjs b/scripts/fixtures/codebook-synthetic.mjs new file mode 100644 index 0000000..578bbc5 --- /dev/null +++ b/scripts/fixtures/codebook-synthetic.mjs @@ -0,0 +1,102 @@ +// Entirely invented data. Source archives only informed the structural categories; +// no task identifiers, text, rules, pictures or formulas were copied from them. +export const image = 'iVBORw0KGgoAAAANSUhEUgAAACgAAAAUCAIAAABwJOjsAAAAJElEQVR4nGOQSDgwIIhh1OJRi0ctHrV41OJRi0ctHrV45FgMAHBlzy43YLJ+AAAAAElFTkSuQmCC'; +export const booleanOptions = ['hasOnlyManualCoding', 'hasClosedVars', 'hasOnlyVarsWithCodes', 'hasDerivedVars', + 'hasGeneralInstructions', 'codeLabelToUpper', 'showScore', 'hideItemVarRelation']; +export const defaults = { exportFormat: 'docx', missingsProfile: '', hasOnlyManualCoding: true, + hasClosedVars: true, hasOnlyVarsWithCodes: true, hasDerivedVars: true, hasGeneralInstructions: true, + codeLabelToUpper: true, showScore: true, hideItemVarRelation: true, trainingRequirement: 'all' }; +export function* optionMatrix() { + for (let bits = 0; bits < 256; bits += 1) { + for (const trainingRequirement of ['all', 'required', 'not-required']) { + yield { ...defaults, ...Object.fromEntries(booleanOptions.map((key, i) => [key, !!(bits & (1 << i))])), trainingRequirement }; + } + } +} +export const missingCases = [[], [{ code: 0, label: 'Missing zero', description: 'Keine Eingabe' }, + { code: 99, label: 'Missing other', description: 'Nicht bearbeitet' }], +[{ code: '0', label: 'Missing string zero', description: 'Keine Eingabe' }]]; +export const ruleCases = [ + ['MATCH', ['alpha']], ['MATCH_REGEX', ['^beta$']], ['NUMERIC_MATCH', ['7']], + ['NUMERIC_RANGE', ['2', '9']], ['NUMERIC_FULL_RANGE', ['3', '8']], + ['NUMERIC_LESS_THAN', ['5']], ['NUMERIC_MORE_THAN', ['6']], ['NUMERIC_MAX', ['10']], + ['NUMERIC_MIN', ['4']], ['IS_EMPTY', []], ['IS_NULL', []], ['IS_TRUE', []], ['IS_FALSE', []] +]; +const code = (id, manualInstruction = '', type = 'FULL_CREDIT', extra = {}) => ({ id, type, + label: `Label ${id}`, score: id === 0 ? 0 : 2, manualInstruction, ruleSets: [], ruleSetOperatorAnd: true, ...extra }); + +// Expected membership is explicit: [no filter, manual only, closed only, both]. +// These declarations are the specification, not a call into the production filter. +const groups = [ + { name: 'MANUAL', codes: [code(2, '

    Bewerten

    '), code(0)], selected: [[2, 0], [2], null, [2, 0]] }, + { name: 'MIXED', codes: [code(2, '

    Gemischt

    '), code(0, '', 'RESIDUAL_AUTO')], selected: [[2, 0], [2], [2, 0], [2, 0]] }, + { name: 'CLOSED', codes: [code(0, '', 'RESIDUAL_AUTO')], selected: [[0], null, [0], [0]] }, + { name: 'INCOMPLETE', codes: [code(0, '', 'INTENDED_INCOMPLETE')], selected: [[0], null, [0], [0]] }, + { name: 'AUTO', codes: [code(0)], selected: [[0], null, null, null] }, + { name: 'EMPTY', codes: [], selected: [[], null, null, null] }, + { name: 'GENERAL_ONLY', codes: [code(0)], general: '

    Allgemeiner Hinweis

    ', selected: [[0], null, null, null] }, + { name: 'INVISIBLE', codes: [code(0, '

     ⁠­

    ')], selected: [[0], null, null, null] }, + { name: 'IMAGE', codes: [code(0, ``)], selected: [[0], [0], null, [0]] }, + { name: 'FORMULA', codes: [code(0, '

    [[iqb-math:x^2]]

    ')], selected: [[0], [0], null, [0]] }, + { name: 'RESIDUAL', codes: [code(0, '

    Rest bewerten

    ', 'RESIDUAL')], selected: [[0], [0], null, [0]] }, + { name: 'TO_CHECK', codes: [code(0, '
    • Prüfen

    ', 'TO_CHECK')], selected: [[0], [0], null, [0]] }, + { name: 'PARTIAL', codes: [code(0, '

    Teilweise

    ', 'PARTIAL_CREDIT')], selected: [[0], [0], null, [0]] }, + { name: 'CLOSED_TEXT', codes: [code(0, '

    Geschlossener Hinweis

    ', 'RESIDUAL_AUTO')], selected: [[0], null, [0], [0]] } +]; +export function selectionCorpus() { + const rows = []; + for (const sourceType of ['BASE', 'MANUAL', 'COPY_VALUE', 'SUM_SCORE', 'SOLVER', 'UNIQUE_VALUES', 'CONCAT_CODE', 'SUM_CODE', 'BASE_NO_VALUE']) { + for (const training of [false, true]) { + for (const group of groups) { + const id = `${sourceType}_${training ? 'TRAIN' : 'REGULAR'}_${group.name}`; + const variable = { id, alias: `PUBLIC.${id}`, label: id, sourceType, deriveSources: ['SOURCE'], + sourceParameters: sourceType === 'SOLVER' ? { solverExpression: 'SOURCE + 1' } : {}, + processing: training ? ['CODER_TRAINING_REQUIRED'] : [], manualInstruction: group.general || ' ', codes: group.codes }; + rows.push({ variable, selected: group.selected, training }); + } + } + } + return { rows, units: [{ id: 2, key: 'Z', name: 'Leer', scheme: JSON.stringify({ version: '3.4', variableCodings: [] }), metadata: { items: [] } }, + { id: 1, key: 'A', name: 'Künstliche Kategorien', scheme: JSON.stringify({ version: '3.4', variableCodings: rows.map(r => r.variable).reverse() }), + metadata: { items: rows.map(({ variable }) => ({ id: `ITEM_${variable.id}`, variableId: variable.alias })) } }] }; +} +export function expectedSelection(rows, options) { + const column = Number(options.hasOnlyManualCoding) + 2 * Number(options.hasClosedVars); + return rows.filter(({ variable, training, selected }) => selected[column] !== null && + variable.sourceType !== 'BASE_NO_VALUE' && (options.hasDerivedVars || variable.sourceType === 'BASE') && + (options.trainingRequirement === 'all' || training === (options.trainingRequirement === 'required')) && + (!options.hasOnlyVarsWithCodes || selected[column].length)) + .map(({ variable, selected }) => ({ id: variable.alias, codes: selected[column].map(String) })) + .sort((a, b) => a.id < b.id ? -1 : Number(a.id > b.id)); +} + +// Stable original-Studio behaviour. Approved corrections have separate tests. +// In particular no nested marks, loose text, whitespace at mark boundaries, +// corrected item relations, or changed manual-only filtering is hidden here. +export function referenceCorpus() { + const variables = ruleCases.map(([method, parameters], i) => ({ id: `RULE_${String(i).padStart(2, '0')}`, + alias: `ALIAS_${String(i).padStart(2, '0')}`, label: `Regel ${i}`, sourceType: 'BASE', manualInstruction: i ? '' : ' ', + codes: [code(1, '

    Bewerten

    ', 'FULL_CREDIT', { ruleSets: [{ rules: [{ method, parameters }], ruleOperatorAnd: true }] })] })); + variables.push({ id: 'RICH', alias: 'Z_RICH', label: 'Formatierung', sourceType: 'BASE', + manualInstruction: '

    Allgemeiner Hinweis

    ', codes: [code(2, + `

    Farbe

    ` + + '

    Unterstrichen

    Kursiv

    Durchgestrichen

    ' + + '
    • Kriterium eins

    • Kriterium zwei

    ' + + '

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ' + + ``)] }); + variables.push({ id: 'DERIVED', alias: 'Z_SUM', label: 'Abgeleitet', sourceType: 'SUM_SCORE', deriveSources: ['RULE_00'], + manualInstruction: '', codes: [code(3, '

    Summe bewerten

    ')] }); + variables.push({ id: 'CONNECT', alias: 'Z_CONNECT', label: 'Verknüpfungen', sourceType: 'BASE', manualInstruction: '', + codes: [true, false].map((and, i) => code(i + 4, '

    Verknüpft

    ', 'FULL_CREDIT', { ruleSetOperatorAnd: and, + ruleSets: [{ ruleOperatorAnd: and, valueArrayPos: 'ANY', rules: [{ method: 'MATCH', parameters: ['gamma'] }, { method: 'IS_TRUE', fragment: 0 }] }, + { ruleOperatorAnd: and, valueArrayPos: 0, rules: [{ method: 'NUMERIC_MATCH', parameters: ['11'] }] }] })) }); + return [{ id: 1, key: 'A', name: 'Synthetische Studio Referenz', scheme: JSON.stringify({ version: '3.4', variableCodings: variables }), metadata: { items: [] } }, + { id: 2, key: 'B', name: 'Zweite Aufgabe', scheme: JSON.stringify({ version: '3.0', variableCodings: [variables[0]] }), metadata: { items: [] } }]; +} +export function* referenceOptions() { + for (let bits = 0; bits < 32; bits += 1) { + const keys = ['hasGeneralInstructions', 'codeLabelToUpper', 'showScore', 'hideItemVarRelation', 'hasDerivedVars']; + yield { ...defaults, hasOnlyManualCoding: false, hasClosedVars: false, hasOnlyVarsWithCodes: false, + ...Object.fromEntries(keys.map((key, i) => [key, !!(bits & (1 << i))])) }; + } +} diff --git a/scripts/fixtures/studio-codebook/README.md b/scripts/fixtures/studio-codebook/README.md index ff24e02..82c3c00 100644 --- a/scripts/fixtures/studio-codebook/README.md +++ b/scripts/fixtures/studio-codebook/README.md @@ -11,3 +11,32 @@ Only two baseline normalizations: Document XML, styles, numbering, settings and footer must otherwise match exactly. The fixture deliberately excludes the separately tested approved corrections (code/missing 0, item relationships, plain instructions and whitespace boundaries). + +## Synthetic raw-schema reference + +`synthetic-reference.json` contains original Studio JSON and SHA-256 digests of +every DOCX part for 32 formatting combinations, starting from raw invented +schemes in `../codebook-synthetic.mjs`. It covers all 13 supported rule methods, +AND/OR rules and rule sets, array positions, multiple tasks, derived variables, +aliases, formatted text, list paragraphs, images and inline/block formulas. +Whitespace-only general instructions must retain the original Studio layout. + +The synthetic categories were informed by five example ZIPs, but none of their +identifiers, task content, instructions, rules or images are included. The example +ZIPs are neither required by tests nor committed as reference fixtures. + +Besides the two normalizations above, ZIP container timestamps are irrelevant, +`docProps/core.xml` (creation/modification metadata) is excluded, and random image +filenames are replaced in first-reference order. Image bytes, relationship +targets, document XML, styles, widths, margins, numbering and page fields remain +part of the exact comparison. `codebook-docx-test-utils.mjs` implements this. + +The baseline is captured explicitly from the immutable Studio commit above: + +```sh +node scripts/capture-studio-codebook-reference.mjs /path/to/studio-checkout +``` + +CI only reads this fixture. Never regenerate it from the shared generator to make +a failing test pass. Approved semantic corrections are checked independently by +the option matrix rather than being disguised as original Studio behaviour. diff --git a/scripts/fixtures/studio-codebook/synthetic-reference.json b/scripts/fixtures/studio-codebook/synthetic-reference.json new file mode 100644 index 0000000..fc45b58 --- /dev/null +++ b/scripts/fixtures/studio-codebook/synthetic-reference.json @@ -0,0 +1,9301 @@ +{ + "source": "iqb-berlin/studio-lite@2504265721a59c7c65425072951ce82ab16f0fb8", + "docx": "8.5.0", + "responses": "5.2.2", + "normalization": [ + "footer date", + "IQB-Studio Codebook -> IQB Codebook", + "exclude docProps/core.xml timestamps", + "random media filenames in first-reference order; image bytes and relationships preserved" + ], + "cases": [ + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": false, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "5bb32bfe61cd729e3f4209fb2229dc59d411975f31f3e505f0375010865301bd", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "99d5f96d9ff75cef3deb0dc265411526598bd0ea7a3f755372ae7761f3b36cc1", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": false, + "codeLabelToUpper": true, + "showScore": false, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "5bb32bfe61cd729e3f4209fb2229dc59d411975f31f3e505f0375010865301bd", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": true, + "codeLabelToUpper": true, + "showScore": false, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "99d5f96d9ff75cef3deb0dc265411526598bd0ea7a3f755372ae7761f3b36cc1", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": false, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "26bb549d5e562916822058d9a3de6914df179b4bc47428988064589310dbf659", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "c50511de7b40e5f30b30b3181abc78a0c6f89ef4d156eb9d63162a84c08a8744", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": false, + "codeLabelToUpper": true, + "showScore": true, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "26bb549d5e562916822058d9a3de6914df179b4bc47428988064589310dbf659", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": true, + "codeLabelToUpper": true, + "showScore": true, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "c50511de7b40e5f30b30b3181abc78a0c6f89ef4d156eb9d63162a84c08a8744", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": false, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "5bb32bfe61cd729e3f4209fb2229dc59d411975f31f3e505f0375010865301bd", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "99d5f96d9ff75cef3deb0dc265411526598bd0ea7a3f755372ae7761f3b36cc1", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": false, + "codeLabelToUpper": true, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "5bb32bfe61cd729e3f4209fb2229dc59d411975f31f3e505f0375010865301bd", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": true, + "codeLabelToUpper": true, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "99d5f96d9ff75cef3deb0dc265411526598bd0ea7a3f755372ae7761f3b36cc1", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": false, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "26bb549d5e562916822058d9a3de6914df179b4bc47428988064589310dbf659", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "c50511de7b40e5f30b30b3181abc78a0c6f89ef4d156eb9d63162a84c08a8744", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": false, + "codeLabelToUpper": true, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "26bb549d5e562916822058d9a3de6914df179b4bc47428988064589310dbf659", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": false, + "hasGeneralInstructions": true, + "codeLabelToUpper": true, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "c50511de7b40e5f30b30b3181abc78a0c6f89ef4d156eb9d63162a84c08a8744", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": false, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "87cfe0ff9c38fcd2c157e548791bae4d76124e957de2d9bdd589f62b16f84c80", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "633c95ee191bf907f7325608a737de635c7d8ee47559bf086a65dd2d951738c7", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": false, + "codeLabelToUpper": true, + "showScore": false, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "87cfe0ff9c38fcd2c157e548791bae4d76124e957de2d9bdd589f62b16f84c80", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": true, + "showScore": false, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "633c95ee191bf907f7325608a737de635c7d8ee47559bf086a65dd2d951738c7", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": false, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "57723ed4c2252405e198c6900dfa4ff453a91b4a3032f2f66b0c7a0d59e0137f", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "70b7903b93334e1302849b9c06cdf99ad810622db701df938e419648acc06f21", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": false, + "codeLabelToUpper": true, + "showScore": true, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "57723ed4c2252405e198c6900dfa4ff453a91b4a3032f2f66b0c7a0d59e0137f", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": true, + "showScore": true, + "hideItemVarRelation": false, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "70b7903b93334e1302849b9c06cdf99ad810622db701df938e419648acc06f21", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": false, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "87cfe0ff9c38fcd2c157e548791bae4d76124e957de2d9bdd589f62b16f84c80", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "633c95ee191bf907f7325608a737de635c7d8ee47559bf086a65dd2d951738c7", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": false, + "codeLabelToUpper": true, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "87cfe0ff9c38fcd2c157e548791bae4d76124e957de2d9bdd589f62b16f84c80", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": true, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    " + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    " + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    " + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    " + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    " + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "633c95ee191bf907f7325608a737de635c7d8ee47559bf086a65dd2d951738c7", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": false, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "57723ed4c2252405e198c6900dfa4ff453a91b4a3032f2f66b0c7a0d59e0137f", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "70b7903b93334e1302849b9c06cdf99ad810622db701df938e419648acc06f21", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": false, + "codeLabelToUpper": true, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "57723ed4c2252405e198c6900dfa4ff453a91b4a3032f2f66b0c7a0d59e0137f", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": true, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "A", + "name": "Synthetische Studio Referenz", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_01", + "label": "Regel 1", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_02", + "label": "Regel 2", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Übereinstimmung (numerisch) mit '7'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_03", + "label": "Regel 3", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '2' und Wert ist maximal '9'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_04", + "label": "Regel 4", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '3' und Wert ist maximal '8'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_05", + "label": "Regel 5", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert geringer als '5'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_06", + "label": "Regel 6", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert größer als '6'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_07", + "label": "Regel 7", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist maximal '10'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_08", + "label": "Regel 8", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist mindestens '4'

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_09", + "label": "Regel 9", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Leerer Wert

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_10", + "label": "Regel 10", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist NULL

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_11", + "label": "Regel 11", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist WAHR

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "ALIAS_12", + "label": "Regel 12", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Wert ist FALSCH

    Bewerten

    ", + "score": "2" + } + ] + }, + { + "id": "Z_CONNECT", + "label": "Verknüpfungen", + "sourceType": "BASE", + "generalInstruction": "", + "codes": [ + { + "id": "4", + "label": "RICHTIG", + "description": "

    gamma\n\nUND\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + }, + { + "id": "5", + "label": "RICHTIG", + "description": "

    gamma\n\nODER\n\nWert ist WAHR - F1

    Übereinstimmung (numerisch) mit '11' (A1)

    Verknüpft

    ", + "score": "2" + } + ] + }, + { + "id": "Z_RICH", + "label": "Formatierung", + "sourceType": "BASE", + "generalInstruction": "

    Allgemeiner Hinweis

    ", + "codes": [ + { + "id": "2", + "label": "RICHTIG", + "description": "

    Farbe

    Unterstrichen

    Kursiv

    Durchgestrichen

    • Kriterium eins

    • Kriterium zwei

    [[iqb-math:x^2]]

    Formel:[[iqb-math:a%3Cb]]

    ", + "score": "2" + } + ] + }, + { + "id": "Z_SUM", + "label": "Abgeleitet", + "sourceType": "SUM_SCORE", + "generalInstruction": "", + "codes": [ + { + "id": "3", + "label": "RICHTIG", + "description": "

    Summe bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + }, + { + "key": "B", + "name": "Zweite Aufgabe", + "variables": [ + { + "id": "ALIAS_00", + "label": "Regel 0", + "sourceType": "BASE", + "generalInstruction": " ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    alpha

    Bewerten

    ", + "score": "2" + } + ] + } + ], + "missings": [ + { + "code": 99, + "label": "Missing", + "description": "Keine Antwort" + } + ] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "4f37a9ec8bc6e77ac1635576141af357bad88366fb22e54344e54d7053be66e2", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "70b7903b93334e1302849b9c06cdf99ad810622db701df938e419648acc06f21", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/media/image-0-78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684.png": "78d10360f88bf6fea9f54941ae25397f917c4f081d3b14f7a86d2685f6f5e684", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + } + ] +} From 946585efd810c31d729d82433155e16ef2cf2914 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Mon, 12 Oct 2026 11:47:34 +0200 Subject: [PATCH 12/36] test: compare host installations and prepare Windows Word acceptance --- docs/shared-codebook.md | 38 +++++- scripts/capture-studio-codebook-reference.mjs | 8 +- scripts/compare-codebook-installations.mjs | 122 ++++++++++++++++++ scripts/verify-codebook-word-windows.ps1 | 95 ++++++++++++++ 4 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 scripts/compare-codebook-installations.mjs create mode 100644 scripts/verify-codebook-word-windows.ps1 diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md index 6a8aac8..0df630e 100644 --- a/docs/shared-codebook.md +++ b/docs/shared-codebook.md @@ -38,7 +38,9 @@ aber im Codebook-Dialog nicht mehr angeboten (Issue #176). * Leere HTML-Strukturen, unsichtbare Zeichen und Leerzeichen sind keine Instruktion; unterstützte Bilder und Formeln sind eine Instruktion. Allgemeine Instruktionen allein machen keinen Code manuell. -* Code 0 und Missing 0 bleiben erhalten. Leere Exporte liefern `[]` bzw. ein gültiges +* Code 0 und Missing 0 bleiben erhalten. Missing-Codes behalten ihren JSON-Datentyp + aus dem Profil (Zahl oder Zeichenkette); Kodierbox wandelt Zahlen nicht mehr in + Zeichenketten um. Leere Exporte liefern `[]` bzw. ein gültiges DOCX. Fehlerhafte Schemen melden die betroffene Aufgabe. * Aufgaben werden nach Schlüssel, Variablen nach Alias/ID sortiert; Codefolge bleibt. * Schulungsbedarf: alle, nur mit oder nur ohne CODER_TRAINING_REQUIRED; kombiniert @@ -81,8 +83,42 @@ Rollback: vorherige Bibliotheks-/Anwendungsversion; keine Datenmigration. JSON, DOCX-Struktur, Bilder, Formeln, Itembeziehungen und CJS/ESM-Gleichheit. * `npm run test:cc`: Angular-Tests einschließlich Auswahl/Suche/Sperrzustände. * Beide Hosts testen ihre echten Adapter und bestehenden Formelregressionen. +* Die künstlichen Fälle in `scripts/fixtures/codebook-synthetic.mjs` enthalten keine + Aufgabeninhalte aus den Beispiel-ZIPs. Die Matrix durchläuft alle 256 Kombinationen + der acht Ja/Nein-Optionen, drei Schulungsfilter, drei Missing-Konstellationen und + beide Formate: 4.608 Exporte. Alle neun Quelltypen werden in der Datenauswahl geprüft. +* 32 zusätzliche Referenzfälle vergleichen Rohschema → JSON und sämtliche relevanten + DOCX-Bestandteile mit dem ursprünglichen Studio. Die Referenz wird ausschließlich + aus dem dokumentierten alten Studio-Commit erzeugt, niemals aus dem neuen Generator. +* `node scripts/compare-codebook-installations.mjs STUDIO_ROOT BOX_ROOT OUTPUT_DIR` + prüft dieselbe Matrix über Studios DownloadWorkspacesClass und Kodierboxs echten + Generation-Service samt Adapter mit Datenquellen im jeweiligen Anwendungsformat. + Nur Datenbank-/Profilzugriffe werden im Speicher bereitgestellt. Der Vergleich + schließt Bilder, Beziehungen, Styles, Fußzeilen und Missing-Datentypen ein. + Das ist ausdrücklich kein HTTP-, Browser-, Berechtigungs- oder Redis-End-to-End-Test. * Studio-Frontend prüft direkten Export, Ladezustände, Fehlermeldung und Speichersperre. * Kodierbox prüft Scope-Filter, wiederholte Starts, langsame Statusabfragen und Schließen. * Frontend- und Backend-Prüfungen auf dem Entwicklungsrechner nacheinander ausführen. * Vor Ausrollen bleiben visuelle Mehrseiten-DOCX-Abnahme und Host-Browserabläufe einschließlich Netzwerkprüfung verpflichtend. + +### Microsoft Word unter Windows + +Word unter Windows ist ein eigener Abnahmeweg. XML-Gleichheit und LibreOffice- +Rendering ersetzen ihn nicht. Der Installationsvergleich legt künstliche Exporte +sechs DOCX-Dateien und `word-expectations.json` im Ausgabeverzeichnis ab: je Anwendung +einen Filter-/Missing-0-Fall sowie Rich-Text-Fälle mit und ohne Score-Spalte. Die +Rich-Text-Fälle werden zusätzlich direkt gegen die alte Studio-Referenz geprüft. +Auf einem Windows-Rechner mit Desktop-Word: + +```powershell +./scripts/verify-codebook-word-windows.ps1 -Directory C:\Codebook-Pruefung +``` + +Das Skript öffnet alle Dateien regulär und schreibgeschützt, aktualisiert Felder, +prüft Textgleichheit, Seitenzahl, Tabellen, ungeteilte Zeilen, Bilder, Formeln und +Fußzeilen und exportiert sechs PDFs. Es speichert Word-Version und Build im Ergebnis. +Anschließend alle gerenderten Dokumente vollständig visuell prüfen: insbesondere +Spaltenbreiten, Umbrüche, Formeln, Bilder, Schriften und „Seite X von Y“. +`visualAcceptanceComplete` bleibt bis zur tatsächlichen visuellen Abnahme false. +Eine nicht ausgeführte Windows-Prüfung darf nicht als bestanden gemeldet werden. diff --git a/scripts/capture-studio-codebook-reference.mjs b/scripts/capture-studio-codebook-reference.mjs index 3c679f8..9618722 100644 --- a/scripts/capture-studio-codebook-reference.mjs +++ b/scripts/capture-studio-codebook-reference.mjs @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { createRequire, Module } from 'node:module'; -import { writeFile } from 'node:fs/promises'; +import { readFile, writeFile } from 'node:fs/promises'; import { referenceCorpus, referenceOptions } from './fixtures/codebook-synthetic.mjs'; import { docxParts, partHashes } from './codebook-docx-test-utils.mjs'; @@ -13,6 +13,12 @@ const ref = '2504265721a59c7c65425072951ce82ab16f0fb8'; const require = createRequire(import.meta.url); const ts = require('typescript'); const source = path => execFileSync('git', ['show', `${ref}:${path}`], { cwd: studio, encoding: 'utf8' }); +const baselineLock = JSON.parse(source('package-lock.json')); +for (const name of ['docx', '@iqb/responses', 'mathml2omml', 'cheerio', '@iqbspecs/coding-scheme']) { + const installed = JSON.parse(await readFile(new URL(`../node_modules/${name}/package.json`, import.meta.url), 'utf8')); + assert.equal(installed.version, baselineLock.packages[`node_modules/${name}`].version, + `Reference capture requires the original Studio dependency version: ${name}`); +} const math = await import('mathml2omml'); function compile(code, overrides = {}) { const output = ts.transpileModule(code, { compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS, esModuleInterop: true } }).outputText; diff --git a/scripts/compare-codebook-installations.mjs b/scripts/compare-codebook-installations.mjs new file mode 100644 index 0000000..89c0ad2 --- /dev/null +++ b/scripts/compare-codebook-installations.mjs @@ -0,0 +1,122 @@ +// Runs real host preparation and adapters against in-memory persistence boundaries. +// Does not replace an HTTP/browser/Redis acceptance run. +import assert from 'node:assert/strict'; +import { createRequire, Module } from 'node:module'; +import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import { resolve, join } from 'node:path'; +import { load } from 'cheerio'; +import { optionMatrix, selectionCorpus, missingCases, expectedSelection, defaults, referenceCorpus, referenceOptions } from './fixtures/codebook-synthetic.mjs'; +import { docxParts, partHashes } from './codebook-docx-test-utils.mjs'; + +const [studioPath, boxPath, outputPath] = process.argv.slice(2); +if (!studioPath || !boxPath || !outputPath) throw new Error('Usage: node scripts/compare-codebook-installations.mjs STUDIO_ROOT BOX_ROOT OUTPUT_DIR'); +const localRequire = createRequire(import.meta.url); +const ts = localRequire('typescript'); +async function hostModule(root, path, overrides = {}) { + const filename = join(root, path); + const require = createRequire(filename); + const source = await readFile(filename, 'utf8'); + const output = ts.transpileModule(source, { compilerOptions: { target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.CommonJS, esModuleInterop: true, experimentalDecorators: true } }).outputText; + const mod = new Module(filename); + mod.filename = filename; + mod.paths = Module._nodeModulePaths(root); + mod.require = name => overrides[name] || require(name); + mod._compile(output, filename); + return mod.exports; +} +const studio = resolve(studioPath); +const box = resolve(boxPath); +const output = resolve(outputPath); +await mkdir(output, { recursive: true }); +const Studio = (await hostModule(studio, 'apps/api/src/app/classes/download-workspaces.class.ts', { + '@studio-lite/shared-code': { isCurrentFromOrder: order => order === 0 } // unrelated metadata reports, never called here +})).DownloadWorkspacesClass; +const boxAdapter = await hostModule(box, 'apps/backend/src/app/admin/code-book/codebook-generator.class.ts'); +// Entity constructors are DI tokens only. Repositories/profiles supply source +// records in host format; the production service, scope handling and adapters run. +const Service = (await hostModule(box, 'apps/backend/src/app/database/services/coding/codebook-generation.service.ts', { + '../../entities/file_upload.entity': { default: class FileUpload {} }, + '../../entities/job-definition.entity': { JobDefinition: class JobDefinition {} }, + '../../entities/variable-bundle.entity': { VariableBundle: class VariableBundle {} }, + './missings-profiles.service': { MissingsProfilesService: class MissingsProfilesService {} }, + '../../../admin/code-book/codebook-generator.class': boxAdapter +})).CodebookGenerationService; +const corpus = selectionCorpus(); +const rows = corpus.rows.filter(({ variable }) => variable.sourceType === 'BASE' || + (variable.id.endsWith('_MIXED') && ['SUM_SCORE', 'BASE_NO_VALUE'].includes(variable.sourceType))); +const rawUnits = [{ ...corpus.units[1], scheme: JSON.stringify({ version: '3.4', variableCodings: rows.map(r => r.variable).reverse() }), + metadata: { items: rows.map(({ variable }) => ({ id: `ITEM_${variable.id}`, variableId: variable.id })) } }]; +let currentUnits = rawUnits; +const service = new Service({ find: async query => { + assert.equal(query.where.workspace_id, 42); + return currentUnits.map(unit => ({ id: unit.id, file_id: `${unit.key}.VOCS`, filename: `${unit.name}.vocs`, data: unit.scheme, + structured_data: { metadata: unit.metadata } })); +} }, { findOne: async () => { throw new Error('Unexpected job definition lookup'); } }, +{ find: async () => { throw new Error('Unexpected bundle lookup'); } }, +{ getMissingsProfileDetails: async (workspace, id) => { + assert.equal(workspace, 42); assert.equal(id, 7); + return { missings: currentMissings }; +} }); +// The logger is not part of generation; keep the 2304-case comparison readable. +service.logger = { log() {}, warn() {}, error(...args) { throw new Error(args.join(' ')); } }; +let currentMissings = []; +let comparisons = 0; +const wordCases = []; +async function compare(options, missings, save = false, suffix = '', reference = null) { + currentMissings = missings; + const current = { ...options, missingsProfile: missings.length ? 'Synthetic profile' : '' }; + const ids = currentUnits.map(unit => unit.id); + const a = await Studio.getWorkspaceCodingBook(42, { findAllWithProperties: async () => currentUnits }, + { findMissingsProfiles: async () => [{ id: 7, label: 'Synthetic profile', missings: JSON.stringify(missings) }] }, current, ids); + const b = await service.generateCodebook(42, missings.length ? 7 : 0, current, ids); + assert.ok(b, 'Box service must return a file'); + if (current.exportFormat === 'json') { + assert.deepEqual(JSON.parse(a.toString()), JSON.parse(b.toString()), 'Studio preparation vs Box service + adapter'); + if (reference) assert.deepEqual(JSON.parse(a.toString()), reference.json, 'installed host vs original Studio JSON'); + else assert.deepEqual(JSON.parse(a.toString())[0].variables.map(({ id, codes }) => ({ id, codes: codes.map(c => c.id) })), expectedSelection(rows, options)); + } else { + const aParts = await docxParts(a); + const bParts = await docxParts(b); + assert.deepEqual(partHashes(aParts), partHashes(bParts), 'complete DOCX parts including images/relationships/footer/styles'); + if (reference) assert.deepEqual(partHashes(aParts), reference.docxParts, 'installed host vs original Studio DOCX'); + const $ = load(aParts['word/document.xml'], { xml: true }); + assert.equal($('w\\:body').children('w\\:p').toArray().some(p => /^\s+$/.test($(p).find('w\\:t').text())), false); + if (save) { + wordCases.push({ suffix, columns: options.showScore ? 4 : 3, tables: $('w\\:tbl').length, + hasMissingZero: missings.some(m => String(m.code) === '0'), + images: $('w\\:drawing').length, formulas: $('m\\:oMath').length, + variableHeadings: $('w\\:pStyle').filter((_, e) => $(e).attr('w:val') === 'Heading2').length }); + } + } + if (save) { + await writeFile(join(output, `studio${suffix}.${current.exportFormat}`), a); + await writeFile(join(output, `kodierbox${suffix}.${current.exportFormat}`), b); + } + comparisons += 1; +} +for (const options of optionMatrix()) { + for (const missings of missingCases) for (const exportFormat of ['json', 'docx']) await compare({ ...options, exportFormat }, missings); + if (comparisons % 600 === 0) console.log(`${comparisons} host export pairs compared`); +} +for (const exportFormat of ['json', 'docx']) await compare({ ...defaults, hideItemVarRelation: false, exportFormat }, missingCases[1], true); +currentUnits = referenceCorpus(); +const references = JSON.parse(await readFile(new URL('./fixtures/studio-codebook/synthetic-reference.json', import.meta.url), 'utf8')).cases; +for (const showScore of [false, true]) { + const options = { ...[...referenceOptions()].at(-1), showScore }; + const reference = references.find(entry => JSON.stringify(entry.options) === JSON.stringify(options)); + assert.ok(reference, 'original Studio reference must exist'); + for (const exportFormat of ['json', 'docx']) await compare({ ...options, exportFormat }, + [{ code: 99, label: 'Missing', description: 'Keine Antwort' }], true, showScore ? '-rich-scores' : '-rich-no-scores', reference); +} +await writeFile(join(output, 'word-expectations.json'), JSON.stringify({ cases: wordCases }, null, 2)); +const roots = [studio, box]; +const artifacts = await Promise.all(roots.map(async root => { + const lock = JSON.parse(await readFile(join(root, 'package-lock.json'), 'utf8')); + return lock.packages['node_modules/@iqb/ngx-coding-components'].integrity; +})); +assert.equal(artifacts[0], artifacts[1], 'both installed artifacts must have the same integrity'); +await writeFile(join(output, 'installation-comparison.json'), JSON.stringify({ hostExportPairs: comparisons, optionCombinations: 768, + missingCases: 3, formats: 2, identicalJson: true, identicalAllDocxParts: true, artifactIntegrity: artifacts[0], + realHostAdapters: true, inMemoryRepositories: true, httpBrowserRedis: false, windowsWord: false }, null, 2)); +console.log(`PASS: ${comparisons} export pairs through real Studio and Box preparation/adapters.`); diff --git a/scripts/verify-codebook-word-windows.ps1 b/scripts/verify-codebook-word-windows.ps1 new file mode 100644 index 0000000..3566144 --- /dev/null +++ b/scripts/verify-codebook-word-windows.ps1 @@ -0,0 +1,95 @@ +param( + [Parameter(Mandatory = $true)][string]$Directory +) +# Run on Windows with desktop Microsoft Word installed. Opens copies read-only; +# never saves changes to the DOCX sources. PDF and JSON are acceptance evidence, +# not a replacement for inspecting the rendered pages. +$ErrorActionPreference = 'Stop' +if ($env:OS -ne 'Windows_NT') { throw 'This check requires Microsoft Word on Windows.' } +$folder = (Resolve-Path $Directory).Path +$manifest = Get-Content (Join-Path $folder 'word-expectations.json') -Raw | ConvertFrom-Json +if (@($manifest.cases).Count -ne 3) { throw 'Expected three Word acceptance cases.' } +$suffixes = @($manifest.cases | ForEach-Object { $_.suffix } | Sort-Object -Unique) +if (($suffixes -join ',') -ne ',-rich-no-scores,-rich-scores') { throw 'Unexpected Word acceptance cases.' } +$word = $null +$results = @() +try { + $word = New-Object -ComObject Word.Application + $word.Visible = $false + $word.DisplayAlerts = 0 + $word.AutomationSecurity = 3 + foreach ($expected in $manifest.cases) { + foreach ($hostName in @('studio', 'kodierbox')) { + $name = "$hostName$($expected.suffix)" + $document = $null + try { + $path = Join-Path $folder "$name.docx" + # ConfirmConversions=false, ReadOnly=true, AddToRecentFiles=false. + # Open normally; do not request Word's OpenAndRepair mode. + $document = $word.Documents.Open($path, $false, $true, $false) + $document.Repaginate() + if ($document.Fields.Update() -ne 0) { throw "$name could not update all document fields." } + foreach ($section in $document.Sections) { + foreach ($footer in $section.Footers) { + if ($footer.Range.Fields.Update() -ne 0) { throw "$name could not update all footer fields." } + } + } + $document.Repaginate() + $pages = $document.ComputeStatistics(2) + $tables = $document.Tables.Count + $images = $document.InlineShapes.Count + $formulas = $document.OMaths.Count + $headings = @($document.Paragraphs | Where-Object { $_.OutlineLevel -eq 2 }).Count + if ($pages -lt 2) { throw "$name must exercise multiple pages." } + if ($tables -ne $expected.tables) { throw "$name lost tables: $tables / $($expected.tables)" } + if ($images -ne $expected.images) { throw "$name lost images: $images / $($expected.images)" } + if ($formulas -ne $expected.formulas) { throw "$name lost equations: $formulas / $($expected.formulas)" } + if ($headings -ne $expected.variableHeadings) { throw "$name lost variable headings." } + foreach ($table in $document.Tables) { + if ($table.Columns.Count -ne $expected.columns) { throw "$name has an incorrect table column count." } + if ($table.Rows.AllowBreakAcrossPages -ne 0) { throw "$name allows code rows to split across pages." } + } + $footerText = $document.Sections.Item(1).Footers.Item(1).Range.Text + if (-not $footerText.Contains('IQB Codebook')) { throw "$name is missing the footer label." } + $footerFields = $document.Sections.Item(1).Footers.Item(1).Range.Fields + $pageFields = @($footerFields | Where-Object { $_.Type -eq 33 }) # wdFieldPage + $totalFields = @($footerFields | Where-Object { $_.Type -eq 26 }) # wdFieldNumPages + if ($pageFields.Count -ne 1 -or $totalFields.Count -ne 1) { throw "$name lost PAGE or NUMPAGES." } + if ([int]$totalFields[0].Result.Text.Trim() -ne $pages) { throw "$name has an incorrect total page field." } + $pageValue = [int]$pageFields[0].Result.Text.Trim() + if ($pageValue -lt 1 -or $pageValue -gt $pages) { throw "$name has an incorrect current page field." } + $text = $document.Content.Text + if ($expected.hasMissingZero -and -not $text.Contains('0 Missing zero')) { throw "$name is missing Missing 0." } + $document.ExportAsFixedFormat((Join-Path $folder "$name-word-windows.pdf"), 17) + $results += [pscustomobject]@{ + case = $expected.suffix; file = "$name.docx"; pages = $pages; tables = $tables; images = $images; formulas = $formulas + text = $text; footer = $footerText; wordVersion = $word.Version; wordBuild = $word.Build + } + } finally { + if ($null -ne $document) { + $document.Close(0) + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($document) + } + } + } + } + foreach ($group in @($results | Group-Object -Property case)) { + $pair = $group.Group + if ($pair.Count -ne 2) { throw 'Missing host document in Word comparison.' } + if ($pair[0].text -cne $pair[1].text) { throw "$($group.Name): different content in Word." } + if ($pair[0].pages -ne $pair[1].pages) { throw "$($group.Name): different page counts in Word." } + if ($pair[0].footer -cne $pair[1].footer) { throw "$($group.Name): different footers in Word." } + } + $report = [pscustomobject]@{ + checkedAtUtc = [DateTime]::UtcNow.ToString('o'); platform = 'Windows desktop Word' + automatedChecksPassed = $true; visualAcceptanceComplete = $false + documents = @($results | Select-Object case,file,pages,tables,images,formulas,wordVersion,wordBuild) + } + $report | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $folder 'word-windows-result.json') -Encoding UTF8 + Write-Output 'Word checks passed. Inspect all six PDFs for page breaks, fonts, formulas, images and page fields before visual acceptance.' +} finally { + if ($null -ne $word) { + $word.Quit(0) + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($word) + } +} From aa404bc2a0a1402c13296db6f3acfd6fed192f9e Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:49:47 +0200 Subject: [PATCH 13/36] fix: preserve protected spaces in Studio codebook exports --- docs/shared-codebook.md | 6 + package.json | 2 +- .../codebook-docx-generator.ts | 3 +- scripts/capture-studio-codebook-reference.mjs | 12 + scripts/codebook-whitespace.test.mjs | 34 + scripts/compare-codebook-installations.mjs | 13 +- scripts/fixtures/codebook-whitespace.mjs | 33 + scripts/fixtures/studio-codebook/README.md | 14 + .../studio-codebook/whitespace-reference.json | 1183 +++++++++++++++++ 9 files changed, 1297 insertions(+), 3 deletions(-) create mode 100644 scripts/codebook-whitespace.test.mjs create mode 100644 scripts/fixtures/codebook-whitespace.mjs create mode 100644 scripts/fixtures/studio-codebook/whitespace-reference.json diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md index 0df630e..f89971a 100644 --- a/docs/shared-codebook.md +++ b/docs/shared-codebook.md @@ -90,11 +90,17 @@ Rollback: vorherige Bibliotheks-/Anwendungsversion; keine Datenmigration. * 32 zusätzliche Referenzfälle vergleichen Rohschema → JSON und sämtliche relevanten DOCX-Bestandteile mit dem ursprünglichen Studio. Die Referenz wird ausschließlich aus dem dokumentierten alten Studio-Commit erzeugt, niemals aus dem neuen Generator. +* Weitere 20 Studio-Referenzfälle erhalten NBSP, schmale geschützte Leerzeichen und + wiederholte Abstände in allgemeinen und Code-Instruktionen, jeweils mit und ohne + Score-Spalte. HTML-Zeichenreferenzen, Unicode und fett formatierter Text werden + über ESM und CommonJS auf exakte Inhalte und alle DOCX-Bestandteile geprüft. * `node scripts/compare-codebook-installations.mjs STUDIO_ROOT BOX_ROOT OUTPUT_DIR` prüft dieselbe Matrix über Studios DownloadWorkspacesClass und Kodierboxs echten Generation-Service samt Adapter mit Datenquellen im jeweiligen Anwendungsformat. Nur Datenbank-/Profilzugriffe werden im Speicher bereitgestellt. Der Vergleich schließt Bilder, Beziehungen, Styles, Fußzeilen und Missing-Datentypen ein. + Zusätzlich laufen die 20 Abstandsreferenzen in beiden Formaten gegen das alte + Studio; zusammen werden 4.654 Exportpaare verglichen. Das ist ausdrücklich kein HTTP-, Browser-, Berechtigungs- oder Redis-End-to-End-Test. * Studio-Frontend prüft direkten Export, Ladezustände, Fehlermeldung und Speichersperre. * Kodierbox prüft Scope-Filter, wiederholte Starts, langsame Statusabfragen und Schließen. diff --git a/package.json b/package.json index 9376d3c..a1bf46c 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test:schemer:coverage": "ng test schemer --watch=false --browsers=ChromeHeadless --code-coverage", "test:coverage": "npm run test:cc:coverage && npm run test:schemer:coverage", "test:scheme": "node --test scripts/validate-coding-scheme.test.js", - "test:codebook": "node --test scripts/codebook-contract.test.mjs scripts/codebook-matrix.test.mjs", + "test:codebook": "node --test scripts/codebook-contract.test.mjs scripts/codebook-matrix.test.mjs scripts/codebook-whitespace.test.mjs", "test:schemer:responsive": "playwright test --config playwright.schemer.config.cjs" }, "dependencies": { diff --git a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts index 964a75b..49fd647 100644 --- a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts +++ b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts @@ -405,7 +405,8 @@ export class CodebookDocxGenerator { }; nodes.forEach(node => { if (node.type === 'text') { - if (node.data) children.push(new TextRun({ text: node.data.replace(/\s+/g, ' '), ...runStyle })); + // Preserve non-breaking spaces and repeated spacing from Studio instructions. + if (node.data) children.push(new TextRun({ text: node.data, ...runStyle })); return; } if (node.type !== 'tag') return; diff --git a/scripts/capture-studio-codebook-reference.mjs b/scripts/capture-studio-codebook-reference.mjs index 9618722..5a4c2f8 100644 --- a/scripts/capture-studio-codebook-reference.mjs +++ b/scripts/capture-studio-codebook-reference.mjs @@ -5,6 +5,7 @@ import { execFileSync } from 'node:child_process'; import { createRequire, Module } from 'node:module'; import { readFile, writeFile } from 'node:fs/promises'; import { referenceCorpus, referenceOptions } from './fixtures/codebook-synthetic.mjs'; +import { whitespaceReferenceCases, whitespaceReferenceUnits } from './fixtures/codebook-whitespace.mjs'; import { docxParts, partHashes } from './codebook-docx-test-utils.mjs'; const studio = process.argv[2]; @@ -56,3 +57,14 @@ const fixture = { source: `iqb-berlin/studio-lite@${ref}`, docx: '8.5.0', respon 'random media filenames in first-reference order; image bytes and relationships preserved'], cases }; await writeFile(new URL('./fixtures/studio-codebook/synthetic-reference.json', import.meta.url), `${JSON.stringify(fixture, null, 2)}\n`); console.log(`Captured ${cases.length} original Studio JSON/DOCX cases from raw invented schemas.`); + +const whitespaceCases = []; +for (const entry of whitespaceReferenceCases()) { + const data = whitespaceReferenceUnits(entry.html).map(unit => adapter.getCodeBookDataForUnit(unit, entry.options, [])); + const json = data.map(({ key, name, variables, missings }) => ({ key, name, variables, missings })); + const parts = await docxParts(await renderer.getDocXCodebook(data, entry.options)); + whitespaceCases.push({ ...entry, json, docxParts: partHashes(parts) }); +} +await writeFile(new URL('./fixtures/studio-codebook/whitespace-reference.json', import.meta.url), + `${JSON.stringify({ ...fixture, cases: whitespaceCases }, null, 2)}\n`); +console.log(`Captured ${whitespaceCases.length} original Studio spacing cases for code and general instructions.`); diff --git a/scripts/codebook-whitespace.test.mjs b/scripts/codebook-whitespace.test.mjs new file mode 100644 index 0000000..10d0cc5 --- /dev/null +++ b/scripts/codebook-whitespace.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { createRequire } from 'node:module'; +import { readFile } from 'node:fs/promises'; +import { load } from 'cheerio'; +import { CodebookGenerator } from '../dist/ngx-coding-components/fesm2022/iqb-ngx-coding-components-codebook-generator.mjs'; +import { whitespaceReferenceCases, whitespaceReferenceUnits } from './fixtures/codebook-whitespace.mjs'; +import { docxParts, partHashes } from './codebook-docx-test-utils.mjs'; + +const require = createRequire(import.meta.url); +const commonjs = require('../dist/ngx-coding-components/cjs/codebook-generator.cjs'); +const reference = JSON.parse(await readFile(new URL('./fixtures/studio-codebook/whitespace-reference.json', import.meta.url), 'utf8')); +const cases = [...whitespaceReferenceCases()]; +assert.equal(reference.cases.length, cases.length); +assert.equal(reference.source, 'iqb-berlin/studio-lite@2504265721a59c7c65425072951ce82ab16f0fb8'); + +for (const [index, entry] of cases.entries()) { + test(`original Studio spacing ${entry.id}, score=${entry.options.showScore}, ESM/CJS`, async () => { + const expected = reference.cases[index]; + assert.deepEqual({ id: expected.id, html: expected.html, text: expected.text, options: expected.options }, entry); + for (const generator of [CodebookGenerator, commonjs.CodebookGenerator]) { + const units = whitespaceReferenceUnits(entry.html); + const json = await generator.generateCodebook(units, { ...entry.options, exportFormat: 'json' }, []); + assert.deepEqual(JSON.parse(await json.text()), expected.json); + const parts = await docxParts(await generator.generateCodebook(units, entry.options, [])); + assert.deepEqual(partHashes(parts), expected.docxParts, 'all DOCX parts must match original Studio'); + const $ = load(parts['word/document.xml'], { xml: true }); + const paragraphs = $('w\\:body').children('w\\:p').toArray().map(p => $(p).find('w\\:t').text()); + assert.ok(paragraphs.includes(entry.text), 'general instruction must retain its exact spacing'); + const description = $('w\\:tr').first().children('w\\:tc').last().find('w\\:t').toArray().map(t => $(t).text()).join(''); + assert.equal(description, entry.text, 'code instruction must retain its exact spacing'); + } + }); +} diff --git a/scripts/compare-codebook-installations.mjs b/scripts/compare-codebook-installations.mjs index 89c0ad2..a43fe96 100644 --- a/scripts/compare-codebook-installations.mjs +++ b/scripts/compare-codebook-installations.mjs @@ -6,6 +6,7 @@ import { readFile, writeFile, mkdir } from 'node:fs/promises'; import { resolve, join } from 'node:path'; import { load } from 'cheerio'; import { optionMatrix, selectionCorpus, missingCases, expectedSelection, defaults, referenceCorpus, referenceOptions } from './fixtures/codebook-synthetic.mjs'; +import { whitespaceReferenceCases, whitespaceReferenceUnits } from './fixtures/codebook-whitespace.mjs'; import { docxParts, partHashes } from './codebook-docx-test-utils.mjs'; const [studioPath, boxPath, outputPath] = process.argv.slice(2); @@ -109,6 +110,15 @@ for (const showScore of [false, true]) { for (const exportFormat of ['json', 'docx']) await compare({ ...options, exportFormat }, [{ code: 99, label: 'Missing', description: 'Keine Antwort' }], true, showScore ? '-rich-scores' : '-rich-no-scores', reference); } +const whitespaceReferences = JSON.parse(await readFile(new URL('./fixtures/studio-codebook/whitespace-reference.json', import.meta.url), 'utf8')).cases; +let whitespaceCases = 0; +for (const entry of whitespaceReferenceCases()) { + const reference = whitespaceReferences[whitespaceCases++]; + assert.deepEqual({ id: reference.id, html: reference.html, text: reference.text, options: reference.options }, entry); + currentUnits = whitespaceReferenceUnits(entry.html); + for (const exportFormat of ['json', 'docx']) await compare({ ...entry.options, exportFormat }, [], false, '', reference); +} +assert.equal(whitespaceCases, whitespaceReferences.length); await writeFile(join(output, 'word-expectations.json'), JSON.stringify({ cases: wordCases }, null, 2)); const roots = [studio, box]; const artifacts = await Promise.all(roots.map(async root => { @@ -117,6 +127,7 @@ const artifacts = await Promise.all(roots.map(async root => { })); assert.equal(artifacts[0], artifacts[1], 'both installed artifacts must have the same integrity'); await writeFile(join(output, 'installation-comparison.json'), JSON.stringify({ hostExportPairs: comparisons, optionCombinations: 768, - missingCases: 3, formats: 2, identicalJson: true, identicalAllDocxParts: true, artifactIntegrity: artifacts[0], + missingCases: 3, formats: 2, originalStudioWhitespaceCases: whitespaceCases, + identicalJson: true, identicalAllDocxParts: true, artifactIntegrity: artifacts[0], realHostAdapters: true, inMemoryRepositories: true, httpBrowserRedis: false, windowsWord: false }, null, 2)); console.log(`PASS: ${comparisons} export pairs through real Studio and Box preparation/adapters.`); diff --git a/scripts/fixtures/codebook-whitespace.mjs b/scripts/fixtures/codebook-whitespace.mjs new file mode 100644 index 0000000..99d5ac5 --- /dev/null +++ b/scripts/fixtures/codebook-whitespace.mjs @@ -0,0 +1,33 @@ +import { defaults } from './codebook-synthetic.mjs'; + +// Invented typography examples. Internal spacing is unchanged by old Studio; +// the separately tested corrections at formatting boundaries still apply. +const variants = [ + ['nbsp-named', '

    10 kg

    ', '10\u00a0kg'], + ['nbsp-decimal', '

    10 kg

    ', '10\u00a0kg'], + ['nbsp-hex', '

    10 kg

    ', '10\u00a0kg'], + ['nbsp-literal', '

    10\u00a0kg

    ', '10\u00a0kg'], + ['narrow-nbsp-decimal', '

    20 cm

    ', '20\u202fcm'], + ['narrow-nbsp-hex', '

    20 cm

    ', '20\u202fcm'], + ['narrow-nbsp-literal', '

    20\u202fcm

    ', '20\u202fcm'], + ['repeated-nbsp', '

    10  kg

    ', '10\u00a0\u00a0kg'], + ['formatted-nbsp', '

    10 kg

    ', '10\u00a0kg'], + ['repeated-spaces', '

    Abstand bleibt

    ', 'Abstand bleibt'] +]; + +export function whitespaceReferenceUnits(html) { + return [{ id: 1, key: 'SPACE', name: 'Synthetische Abstände', metadata: { items: [] }, + scheme: JSON.stringify({ version: '3.4', variableCodings: [{ id: 'V', label: 'Abstände', + sourceType: 'BASE', manualInstruction: html, + codes: [{ id: 1, type: 'FULL_CREDIT', label: 'Code', score: 1, + ruleSets: [], ruleSetOperatorAnd: true, manualInstruction: html }] }] }) }]; +} + +export function* whitespaceReferenceCases() { + for (const [id, html, text] of variants) { + for (const showScore of [false, true]) { + yield { id, html, text, options: { ...defaults, hasOnlyManualCoding: false, + hasClosedVars: false, hasOnlyVarsWithCodes: false, codeLabelToUpper: false, showScore } }; + } + } +} diff --git a/scripts/fixtures/studio-codebook/README.md b/scripts/fixtures/studio-codebook/README.md index 82c3c00..ce22e4c 100644 --- a/scripts/fixtures/studio-codebook/README.md +++ b/scripts/fixtures/studio-codebook/README.md @@ -40,3 +40,17 @@ node scripts/capture-studio-codebook-reference.mjs /path/to/studio-checkout CI only reads this fixture. Never regenerate it from the shared generator to make a failing test pass. Approved semantic corrections are checked independently by the option matrix rather than being disguised as original Studio behaviour. + +## Protected spacing reference + +`whitespace-reference.json` is captured by the same command from the same old +Studio commit. Its 20 cases cover NBSP and narrow NBSP as HTML entities, numeric +character references and literal Unicode, repeated NBSP, bold text and repeated +ordinary spaces, each with and without a score column. Every input places the +same text in both a general instruction and a code instruction. + +The tests compare original Studio JSON and every normalized DOCX part against +both ESM and CommonJS exports and additionally assert the exact instruction text. +The host installation comparison also runs all 20 cases in JSON and DOCX through +both installed adapters against this independent reference. These cases extend +the references without changing the original 32-case fixture. diff --git a/scripts/fixtures/studio-codebook/whitespace-reference.json b/scripts/fixtures/studio-codebook/whitespace-reference.json new file mode 100644 index 0000000..b612398 --- /dev/null +++ b/scripts/fixtures/studio-codebook/whitespace-reference.json @@ -0,0 +1,1183 @@ +{ + "source": "iqb-berlin/studio-lite@2504265721a59c7c65425072951ce82ab16f0fb8", + "docx": "8.5.0", + "responses": "5.2.2", + "normalization": [ + "footer date", + "IQB-Studio Codebook -> IQB Codebook", + "exclude docProps/core.xml timestamps", + "random media filenames in first-reference order; image bytes and relationships preserved" + ], + "cases": [ + { + "id": "nbsp-named", + "html": "

    10 kg

    ", + "text": "10 kg", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    10 kg

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    10 kg

    " + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "d5f9f53a5031c8a9ddd1e3a34b7ee7736fc65b91bc282eddaf594b77cbfe1a0c", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "nbsp-named", + "html": "

    10 kg

    ", + "text": "10 kg", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    10 kg

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    10 kg

    ", + "score": "1" + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "c84f505c7595bf22b934aae1101ee06805d326f0f5c4796d3cf2132c93391312", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "nbsp-decimal", + "html": "

    10 kg

    ", + "text": "10 kg", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    10 kg

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    10 kg

    " + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "d5f9f53a5031c8a9ddd1e3a34b7ee7736fc65b91bc282eddaf594b77cbfe1a0c", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "nbsp-decimal", + "html": "

    10 kg

    ", + "text": "10 kg", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    10 kg

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    10 kg

    ", + "score": "1" + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "c84f505c7595bf22b934aae1101ee06805d326f0f5c4796d3cf2132c93391312", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "nbsp-hex", + "html": "

    10 kg

    ", + "text": "10 kg", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    10 kg

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    10 kg

    " + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "d5f9f53a5031c8a9ddd1e3a34b7ee7736fc65b91bc282eddaf594b77cbfe1a0c", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "nbsp-hex", + "html": "

    10 kg

    ", + "text": "10 kg", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    10 kg

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    10 kg

    ", + "score": "1" + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "c84f505c7595bf22b934aae1101ee06805d326f0f5c4796d3cf2132c93391312", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "nbsp-literal", + "html": "

    10 kg

    ", + "text": "10 kg", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    10 kg

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    10 kg

    " + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "d5f9f53a5031c8a9ddd1e3a34b7ee7736fc65b91bc282eddaf594b77cbfe1a0c", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "nbsp-literal", + "html": "

    10 kg

    ", + "text": "10 kg", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    10 kg

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    10 kg

    ", + "score": "1" + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "c84f505c7595bf22b934aae1101ee06805d326f0f5c4796d3cf2132c93391312", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "narrow-nbsp-decimal", + "html": "

    20 cm

    ", + "text": "20 cm", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    20 cm

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    20 cm

    " + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "2fbb32bd9464923bcb49d0204245fa54a7f3bab9bf6091260c2ed413b48fa745", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "narrow-nbsp-decimal", + "html": "

    20 cm

    ", + "text": "20 cm", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    20 cm

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    20 cm

    ", + "score": "1" + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "d22b462f11aa99bb4342156f65b68b5e73092a2abd7fbc728e840519384a9e76", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "narrow-nbsp-hex", + "html": "

    20 cm

    ", + "text": "20 cm", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    20 cm

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    20 cm

    " + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "2fbb32bd9464923bcb49d0204245fa54a7f3bab9bf6091260c2ed413b48fa745", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "narrow-nbsp-hex", + "html": "

    20 cm

    ", + "text": "20 cm", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    20 cm

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    20 cm

    ", + "score": "1" + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "d22b462f11aa99bb4342156f65b68b5e73092a2abd7fbc728e840519384a9e76", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "narrow-nbsp-literal", + "html": "

    20 cm

    ", + "text": "20 cm", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    20 cm

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    20 cm

    " + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "2fbb32bd9464923bcb49d0204245fa54a7f3bab9bf6091260c2ed413b48fa745", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "narrow-nbsp-literal", + "html": "

    20 cm

    ", + "text": "20 cm", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    20 cm

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    20 cm

    ", + "score": "1" + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "d22b462f11aa99bb4342156f65b68b5e73092a2abd7fbc728e840519384a9e76", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "repeated-nbsp", + "html": "

    10  kg

    ", + "text": "10  kg", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    10  kg

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    10  kg

    " + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "98e0a6b2f6a6517823bde18586896aa2d7f7e726ca30c3ce400b22de7a34d00d", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "repeated-nbsp", + "html": "

    10  kg

    ", + "text": "10  kg", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    10  kg

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    10  kg

    ", + "score": "1" + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "3895359b0adbabd51aaa6e49e1b5ed55c532cca9184318cfc32fd9f31e0bc4bd", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "formatted-nbsp", + "html": "

    10 kg

    ", + "text": "10 kg", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    10 kg

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    10 kg

    " + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "38224679ebf17932f60df3476af6d166cb9ead25bed982aef11d90bca1c1250c", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "formatted-nbsp", + "html": "

    10 kg

    ", + "text": "10 kg", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    10 kg

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    10 kg

    ", + "score": "1" + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "809580cfa42b32d2b96e75f046ecc9753c28a560e7090f8992754f346f1d61e8", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "repeated-spaces", + "html": "

    Abstand bleibt

    ", + "text": "Abstand bleibt", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": false, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    Abstand bleibt

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Abstand bleibt

    " + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "6648dae92f513a0a1a1e2f6e76cdcdb1bd5265853c9ae43b14f10e5f8ffa4040", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + }, + { + "id": "repeated-spaces", + "html": "

    Abstand bleibt

    ", + "text": "Abstand bleibt", + "options": { + "exportFormat": "docx", + "missingsProfile": "", + "hasOnlyManualCoding": false, + "hasClosedVars": false, + "hasOnlyVarsWithCodes": false, + "hasDerivedVars": true, + "hasGeneralInstructions": true, + "codeLabelToUpper": false, + "showScore": true, + "hideItemVarRelation": true, + "trainingRequirement": "all" + }, + "json": [ + { + "key": "SPACE", + "name": "Synthetische Abstände", + "variables": [ + { + "id": "V", + "label": "Abstände", + "sourceType": "BASE", + "generalInstruction": "

    Abstand bleibt

    ", + "codes": [ + { + "id": "1", + "label": "RICHTIG", + "description": "

    Abstand bleibt

    ", + "score": "1" + } + ] + } + ], + "missings": [] + } + ], + "docxParts": { + "_rels/.rels": "641d22a23aa9eb550103a7a01471eaeda5e84c69413e6e03e7fd290b4811a756", + "[Content_Types].xml": "df5eb4247e8594a57c753483112f94648c5b3c162ca0219282d2fff19332bc95", + "docProps/app.xml": "579eba9a8d602575736c3880cf5b529aabbadee0e8cc0e4df8573db0ceadbd4b", + "docProps/custom.xml": "8ec73cc7ce297e42cc652a1d1884d4ffb18ffa0ba7a6502a36f8da90d1be61a1", + "word/_rels/document.xml.rels": "aa8797f9c50610a37b43a791788cb6fe01a8527573c99c09bc946fff7bc31db3", + "word/_rels/fontTable.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footer1.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/_rels/footnotes.xml.rels": "bf64631f0bc7bd46712fef822bcfb22e069fa5779e133980ca0c9d81f00efc09", + "word/comments.xml": "dd2d62f68c6fd01fc4d7d44081587af54ea3e135b9486cc44fa3c62d087adfec", + "word/document.xml": "dfecbaf8f9be0b973a6651243b6b4f543017cff17adc3655762cc6ed9fef0c51", + "word/fontTable.xml": "a746dbb577ec5d68983d659707c0750ca89d40b9d3316e31af331d5e1c736bad", + "word/footer1.xml": "30efed85f494baa349a2bb3df6b05b5dc032d5e912fc5e3aa5d8b293ceea2127", + "word/footnotes.xml": "5a340540bed5ec7a6a1087be13fb0a226151e22e28c43a57d6e5f02acd53ba02", + "word/numbering.xml": "d7283fe87395f6e1c8f2d7f9ece168d2fb35865df19c2ad0660b1390805433fe", + "word/settings.xml": "9f0ff5915faa60180bf7eb19e0db13633fb19c5cffce8d36c7b9a8387f067ad8", + "word/styles.xml": "c51ec0d97223a6b40d98f915b1886d85a909998d4fecdb66ce8bd9be8ef68582" + } + } + ] +} From d9182dc1046dee9725eca0c114031711088e8c57 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:13:44 +0200 Subject: [PATCH 14/36] test: retain synthetic documents for Office rendering acceptance --- scripts/fixtures/word-acceptance/README.md | 20 ++++++++++++++++++ .../word-acceptance/rich-no-scores.docx | Bin 0 -> 10635 bytes .../fixtures/word-acceptance/rich-scores.docx | Bin 0 -> 10759 bytes .../word-acceptance/studio-browser.docx | Bin 0 -> 10827 bytes 4 files changed, 20 insertions(+) create mode 100644 scripts/fixtures/word-acceptance/README.md create mode 100644 scripts/fixtures/word-acceptance/rich-no-scores.docx create mode 100644 scripts/fixtures/word-acceptance/rich-scores.docx create mode 100644 scripts/fixtures/word-acceptance/studio-browser.docx diff --git a/scripts/fixtures/word-acceptance/README.md b/scripts/fixtures/word-acceptance/README.md new file mode 100644 index 0000000..1275888 --- /dev/null +++ b/scripts/fixtures/word-acceptance/README.md @@ -0,0 +1,20 @@ +# Synthetic Office acceptance documents + +These documents contain invented test data only. They are retained at a public, +versioned URL so Microsoft Office Web Viewer can render the actual DOCX files +without uploading user data to an Office account. + +- `rich-scores.docx` and `rich-no-scores.docx` are outputs of + `scripts/compare-codebook-installations.mjs` using the installed shared package + at commit `aa404bc2`. Every relevant DOCX part was compared with the immutable + original Studio reference. The corresponding Kodierbox outputs have identical + normalized document parts. +- `studio-browser.docx` is an actual browser download from Studio with its real + production backend and a fresh migrated test database. It includes the rich + corpus plus NBSP, narrow NBSP and repeated spaces in general/code instructions. + +Source fixtures: `../codebook-synthetic.mjs`, `../codebook-whitespace.mjs`. +Package SHA256: `dcee6cc39436baf91cf2558afedfbc202e044ca14dbf72c7bfdf4b381e5c0ccd`. + +These files are inputs to visual acceptance, not proof that Office has rendered +or passed them. Execution results are documented separately. diff --git a/scripts/fixtures/word-acceptance/rich-no-scores.docx b/scripts/fixtures/word-acceptance/rich-no-scores.docx new file mode 100644 index 0000000000000000000000000000000000000000..2767a4f6497171e9293bb4b590b9fcd7db40fd38 GIT binary patch literal 10635 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∈JZF`GjP*_8~tf`KBI_W5pN zR{-?Zr)oGTIv=l23Fp5!#z>4=okC9UdmXXrZ*6@oV^=MEF^NQ{Y+;{3gCS#PL3{e? ztaj*eLKh@;8>b@Xd{OULE9(H3!nkd@LD)Ct1daKx-o-rOihQqxwKi>rs^Q=Wgfp%$ zGH1Xb)`+^MeR62}$rqhg1l$v9m$U+6_*T|lixkQno1Wql0kIavDRmiTQE*%qxLx(w z>DIQbF2oUR07B26jZLC|Zd6wlV>Pa^a$_lYm{v+bgeZ;_8A-qB#x4A8oO+fq>b6Ak zc>04rd4Ip?qz*6Y; z^mf`Jg;QSCTIUX`voupJBgMc2p+IZZIIbo8I#XbrZ6KfmMxh)}O3{LwRW{i1j1&38 zrh)B{$-F=7XKpwYdRI?o@7LU1IUD9O&BFupUVp1yXaHAd7ic-VVgFc8eQV461Na}y zIk{-JMuG#qqHfEn67x!dW1{MD>fW)-JbcERcD8GTjIoc}@|5C;R*IB4|Kurs#Wj*%Kv>q}QTz)F0|>7+O9aeE za%pX3kp(9fg?^faunm$v9))7SZ1Fo)jbtj!a))%P5`EKmRKjJkt*F^-@waO-xZWvu zrl)cwN3Nri$mSd8FxGWE?yPhWzIE49VjFpG&x`vkR*B$*O)9CLzhvjcQPupEc;kWH z)G(<1tlclU;&!07vOUm>$E$<@4|vWPR~2SHKa*T|5lT`LCBxt2MD7y*Win%ss9-+D>CmfTN3f{j-CJo-VUfo~GHWS(0LPw4Q zV4%wBaUZlIcb}RG@a~QducFtA*z_;08f;|yF;Sx6N1)U5aKWA zV&$Nyt7q}O43j#xXw^=F0kf|{x%|smDHH8g)d^F z9NukuSe&WY?DjxbY|}NX8KvNs$*Fr4pP$Y2=yl)4<&|G}5!s({*#QveUO@a$wUpV%L4DZ@_3{W&8zU z-~Q8P%$lB#&dSNL%_Hz}JOTRlHb@G1MXg3KdD=9_2vSV@n8z-!aHt(9v|KS}_e$w% ztHFm|(AqlWqTIAbcy$#B<@kK_iqZ$T^ZcklB`q*;48VWiJpwTXYWK0-ef#Go(tktl zXZ)kxL&!nTAO7|CyZ-mxq<@g0FR8zQps@1qn@SHUJ-nUsn^GKT|9xlSPZ|&54^Iw$ z!#O}_3E$zr&k=ru|GnV%CD|YCe){;Jk6#h-Z`hB?KP=Jy1Ofmr{)GK3*gk|mEI$5* z=YgVy?=1gSiu{fLH`n*op&#v@YY~i}b)esvA1ptt7X3s40A{~1-?!gG;GZ$qeO={u zbH)5iL;k2@{7*s;OCP@p5o7&B=noPP*?5>T|3*{d{LcjXA%TY(+iwD&aQ|3p{a^Xp zLjn);rQZY!@%|w2PxkZ>{V-YijW!2K_4}ItmAX8{|G5nJq3`de3HtXt{&%tK??Ct= zsfW?&Z&FZ1|B(7KR(%M67!Cb~PZIwF|1&mv2!H5Uf5Ur8e+l9bpYRa>--5sdnXJDA z@duR$8u{;i^tbVA`bp}i@&9Qxe{80Q=KuE`w*ChGr!$a~g!;a7u>shCkDx5U?cR$3 F{vTu%SS$bl literal 0 HcmV?d00001 From 88507b007b216148496d4fdb2e1afa76441afa0b Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:15:49 +0200 Subject: [PATCH 15/36] test: add original Studio documents for Office comparison --- scripts/fixtures/word-acceptance/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/fixtures/word-acceptance/README.md b/scripts/fixtures/word-acceptance/README.md index 1275888..ec33017 100644 --- a/scripts/fixtures/word-acceptance/README.md +++ b/scripts/fixtures/word-acceptance/README.md @@ -18,3 +18,8 @@ Package SHA256: `dcee6cc39436baf91cf2558afedfbc202e044ca14dbf72c7bfdf4b381e5c0cc These files are inputs to visual acceptance, not proof that Office has rendered or passed them. Execution results are documented separately. + +`original-studio-scores.docx` and `original-studio-no-scores.docx` are generated +from the immutable pre-harmonisation Studio renderer and adapter at +`2504265721a59c7c65425072951ce82ab16f0fb8`, using the same synthetic rich corpus +and dependency versions. They support direct same-engine visual comparison. From 6bfa2ca487890cb54a69c861f833f1535e64c7f4 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:20:43 +0200 Subject: [PATCH 16/36] test: include immutable Studio Office baselines --- .../original-studio-no-scores.docx | Bin 0 -> 10639 bytes .../word-acceptance/original-studio-scores.docx | Bin 0 -> 10763 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 scripts/fixtures/word-acceptance/original-studio-no-scores.docx create mode 100644 scripts/fixtures/word-acceptance/original-studio-scores.docx diff --git a/scripts/fixtures/word-acceptance/original-studio-no-scores.docx b/scripts/fixtures/word-acceptance/original-studio-no-scores.docx new file mode 100644 index 0000000000000000000000000000000000000000..97017194fe1719bb33c997236f2e31ab363c555c GIT binary patch literal 10639 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 literal 0 HcmV?d00001 diff --git a/scripts/fixtures/word-acceptance/original-studio-scores.docx b/scripts/fixtures/word-acceptance/original-studio-scores.docx new file mode 100644 index 0000000000000000000000000000000000000000..7f6a5dcb413b9748576ef2fc54f6dbafc813fc1e GIT binary patch literal 10763 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 y=jRFZem`XT%lZHEn*Z$oNACY&4cUJK|1%iKN Date: Tue, 13 Oct 2026 15:24:28 +0200 Subject: [PATCH 17/36] test: cover shared editor formatting combinations --- docs/shared-codebook.md | 31 ++++++++++++++++++ package.json | 2 +- scripts/codebook-editor-matrix.test.mjs | 29 ++++++++++++++++ .../create-codebook-editor-office-fixture.mjs | 19 +++++++++++ scripts/fixtures/codebook-editor-matrix.mjs | 22 +++++++++++++ scripts/fixtures/word-acceptance/README.md | 5 +++ .../word-acceptance/editor-combinations.docx | Bin 0 -> 11293 bytes 7 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 scripts/codebook-editor-matrix.test.mjs create mode 100644 scripts/create-codebook-editor-office-fixture.mjs create mode 100644 scripts/fixtures/codebook-editor-matrix.mjs create mode 100644 scripts/fixtures/word-acceptance/editor-combinations.docx diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md index f89971a..ce5aca1 100644 --- a/docs/shared-codebook.md +++ b/docs/shared-codebook.md @@ -128,3 +128,34 @@ Anschließend alle gerenderten Dokumente vollständig visuell prüfen: insbesond Spaltenbreiten, Umbrüche, Formeln, Bilder, Schriften und „Seite X von Y“. `visualAcceptanceComplete` bleibt bis zur tatsächlichen visuellen Abnahme false. Eine nicht ausgeführte Windows-Prüfung darf nicht als bestanden gemeldet werden. + +### Editor-/Player-Abgleich und Formatkombinationen + +Die Instruktionen des coding-components-Editors sind der Eingabevertrag des +Codebook-Exports. Die Extension-Liste wurde mit Aspect Editor/Player bei +`iqb-berlin/verona-modules-aspect@fb3f5982` abgeglichen. Der Export liest +Kodierschemata (`manualInstruction`), nicht die vollständige Aspect-Aufgabe mit +interaktiven Player-Elementen. + +`codebook-editor-matrix.test.mjs` prüft alle 384 Kombinationen aus vier +Textmarkierungen (Fett, Kursiv, Unterstrichen, Durchgestrichen), drei gültigen +Grundlinienzuständen, zwei Verschachtelungsreihenfolgen und vier Ausrichtungen. +Jeder Fall prüft allgemeine und Code-Instruktion, geschützte und wiederholte +Leerzeichen, Formatierungsgrenzen sowie identische ESM-/CommonJS-Dokumentteile. +Diese Matrix ergänzt die 52 unabhängigen Alt-Studio-Referenzfälle. + +`node scripts/create-codebook-editor-office-fixture.mjs` erzeugt ein ausschließlich +synthetisches Dokument mit allen 48 Markierungskombinationen sowie gemischten +Formel-, Listen- und Bildfällen zur visuellen Office-Abnahme. + +Grenzen des bestehenden Studio-Renderers bleiben sichtbar: CSS-Listenstile, +verschachtelte Listeneinrückungen, Bild-Floats und pro Textlauf wechselnde +Farbe/Schriftgröße werden nicht vollständig wie im Browser abgebildet. Der +Abgleich behauptet deshalb keine vollständige HTML-/CSS-Layouttreue. +Aspect-spezifische Formelknoten (`aspect-nodeview-math-formula[formula]`) und +interaktive Cloze-Knoten sind ein anderer Vertrag als +`span.iqb-math-formula[data-latex]` bzw. `[[iqb-math:…]]` in Kodierinstruktionen. +Der Aspect-Player rendert seine gespeicherten Formeln über +`projects/common/utils/math-formula-markup.ts`; der Codebook-Export nutzt OMML. +Ein vollständiger Aspect-Editor-/Player-Lauf ist damit nicht durch diese +Codebook-Matrix ersetzt. diff --git a/package.json b/package.json index a1bf46c..973befa 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test:schemer:coverage": "ng test schemer --watch=false --browsers=ChromeHeadless --code-coverage", "test:coverage": "npm run test:cc:coverage && npm run test:schemer:coverage", "test:scheme": "node --test scripts/validate-coding-scheme.test.js", - "test:codebook": "node --test scripts/codebook-contract.test.mjs scripts/codebook-matrix.test.mjs scripts/codebook-whitespace.test.mjs", + "test:codebook": "node --test scripts/codebook-contract.test.mjs scripts/codebook-matrix.test.mjs scripts/codebook-whitespace.test.mjs scripts/codebook-editor-matrix.test.mjs", "test:schemer:responsive": "playwright test --config playwright.schemer.config.cjs" }, "dependencies": { diff --git a/scripts/codebook-editor-matrix.test.mjs b/scripts/codebook-editor-matrix.test.mjs new file mode 100644 index 0000000..b1ed39c --- /dev/null +++ b/scripts/codebook-editor-matrix.test.mjs @@ -0,0 +1,29 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { createRequire } from 'node:module'; +import { load } from 'cheerio'; +import { CodebookDocxGenerator } from '../dist/ngx-coding-components/fesm2022/iqb-ngx-coding-components-codebook-generator.mjs'; +import { defaults } from './fixtures/codebook-synthetic.mjs'; +import { editorMarkCases, editorMarkUnits } from './fixtures/codebook-editor-matrix.mjs'; +import { docxParts, partHashes } from './codebook-docx-test-utils.mjs'; +const require = createRequire(import.meta.url); +const cjs = require('../dist/ngx-coding-components/cjs/codebook-generator.cjs'); +for (const entry of editorMarkCases()) test(`editor mark combination ${entry.id}`, async () => { + const units = editorMarkUnits(entry.html); + const parts = await docxParts(await CodebookDocxGenerator.generateDocx(units, defaults)); + const common = await docxParts(await cjs.CodebookDocxGenerator.generateDocx(units, defaults)); + assert.deepEqual(partHashes(parts), partHashes(common), 'ESM and CommonJS document parts'); + const $ = load(parts['word/document.xml'], { xml: true }); + const marked = $('w\\:r').toArray().filter(run => $(run).children('w\\:t').text() .includes(entry.text)); + assert.equal(marked.length, 2, 'general and code instruction each retain exact Unicode/spacing'); + for (const run of marked) { + for (const [bit, tag] of ['b', 'i', 'u', 'strike'].entries()) { + assert.equal($(run).find(`w\\:${tag}`).length > 0, !!(entry.mask & (1 << bit)), tag); + } + assert.equal($(run).find('w\\:vertAlign').attr('w:val'), + entry.vertical ? (entry.vertical === 'sup' ? 'superscript' : 'subscript') : undefined); + const paragraph = $(run).closest('w\\:p'); + assert.equal(paragraph.find('w\\:jc').attr('w:val'), entry.alignment === 'justify' ? 'both' : entry.alignment); + assert.equal(paragraph.find('w\\:t').toArray().map(node => $(node).text()).join(''), `vor ${entry.text} nach`); + } +}); diff --git a/scripts/create-codebook-editor-office-fixture.mjs b/scripts/create-codebook-editor-office-fixture.mjs new file mode 100644 index 0000000..bcc78c2 --- /dev/null +++ b/scripts/create-codebook-editor-office-fixture.mjs @@ -0,0 +1,19 @@ +// Maintainer-only: regenerate the synthetic Office visual-acceptance fixture. +import { writeFile } from 'node:fs/promises'; +import { CodebookDocxGenerator } from '../dist/ngx-coding-components/fesm2022/iqb-ngx-coding-components-codebook-generator.mjs'; +import { editorMarkCases, editorMarkUnits } from './fixtures/codebook-editor-matrix.mjs'; +import { defaults, image } from './fixtures/codebook-synthetic.mjs'; +const entries = [...editorMarkCases()].filter(entry => entry.id.includes('-forward-') && entry.alignment === 'left'); +const units = editorMarkUnits(''); +units[0].variables[0].generalInstruction = '

    Alle 48 gültigen Kombinationen aus vier Textmarkierungen und Grundlinie/Hoch-/Tiefstellung.

    '; +units[0].variables[0].codes = entries.map((entry, index) => ({ + id: String(index + 1), label: entry.id.replace('-forward-left', ''), score: '1', description: entry.html +})); +units[0].variables[0].codes.push( + { id: '49', label: 'Formeln', score: '1', description: '

    Formel: [[iqb-math:x^2]] und

    ' }, + { id: '50', label: 'Listen', score: '1', description: '
    • Liste 10 kg

      • Unterpunkt

    ' }, + { id: '51', label: 'Bild', score: '1', description: `

    vor Bild

    nach Bild

    ` } +); +const blob = await CodebookDocxGenerator.generateDocx(units, defaults); +await writeFile(new URL('./fixtures/word-acceptance/editor-combinations.docx', import.meta.url), Buffer.from(await blob.arrayBuffer())); +console.log('Generated 48 mark combinations plus formula/list/image cases from invented content.'); diff --git a/scripts/fixtures/codebook-editor-matrix.mjs b/scripts/fixtures/codebook-editor-matrix.mjs new file mode 100644 index 0000000..afd1f76 --- /dev/null +++ b/scripts/fixtures/codebook-editor-matrix.mjs @@ -0,0 +1,22 @@ +// Supported inline marks shared by the coding-components instruction editor and +// Aspect's Tiptap editor. Subscript/superscript are mutually exclusive. +export function* editorMarkCases() { + const marks = ['strong', 'em', 'u', 's']; + for (let mask = 0; mask < 16; mask += 1) for (const vertical of ['', 'sub', 'sup']) { + for (const reverse of [false, true]) for (const alignment of ['left', 'center', 'right', 'justify']) { + const tags = marks.filter((_, bit) => mask & (1 << bit)); + if (vertical) tags.push(vertical); + if (reverse) tags.reverse(); + const text = '10\u00a0kg; 20\u202fcm; Abstand bleibt'; + const marked = tags.reduce((html, tag) => `<${tag}>${html}`, text); + yield { id: `${mask}-${vertical || 'base'}-${reverse ? 'reverse' : 'forward'}-${alignment}`, + html: `

    vor ${marked} nach

    `, text, mask, vertical, alignment }; + } + } +} +export function editorMarkUnits(html) { + return [{ key: 'EDITOR', name: 'Synthetische Formatkombinationen', items: [], missings: [], variables: [{ + id: 'V', label: 'Formatkombination', sourceType: 'BASE', generalInstruction: html, + codes: [{ id: '1', label: 'Code', score: '1', description: html }] + }] }]; +} diff --git a/scripts/fixtures/word-acceptance/README.md b/scripts/fixtures/word-acceptance/README.md index ec33017..c7df3b6 100644 --- a/scripts/fixtures/word-acceptance/README.md +++ b/scripts/fixtures/word-acceptance/README.md @@ -23,3 +23,8 @@ or passed them. Execution results are documented separately. from the immutable pre-harmonisation Studio renderer and adapter at `2504265721a59c7c65425072951ce82ab16f0fb8`, using the same synthetic rich corpus and dependency versions. They support direct same-engine visual comparison. + +`editor-combinations.docx` contains all 48 valid shared inline-mark combinations +and mixed formula/list/image cases. Regenerate after `npm run build_cc` with +`node scripts/create-codebook-editor-office-fixture.mjs`. Its executable contract +is `scripts/codebook-editor-matrix.test.mjs` (384 cases, each ESM and CommonJS). diff --git a/scripts/fixtures/word-acceptance/editor-combinations.docx b/scripts/fixtures/word-acceptance/editor-combinations.docx new file mode 100644 index 0000000000000000000000000000000000000000..e82ead5265c9ca3ebdefdcbadf7f5a9c8c8a3188 GIT binary patch literal 11293 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

    ^Xg6%S@RIc-4?GH;n-{a6HxGC2 zrp2??k--I@zs)W@h`YN7P-idPAL=x)vAVlb@k5=HOZMx;I54Z4c3f&8btTUCHI(W5 z-&_|SX8LxrT_I$SH|Z*pOD3g1^B4a}wi1^#)86%7k}aH+J&{%;z%#)QIzb(k;gsFg z=dxwd|D}@z+9Z{vg>4=dAHP5irs@;<-A|p66RKy*klSfx#5KtZexIalE#p;q>km(# z)GpY`JZ3I9=Af&(LedY4ctbgccW!A2>C<6_fcZc^qk|;6@Yu2_P`e1OQQF_LNMb%m z@>WADg#xqE@ij%Mf!Qkx(ek)<)Eu^ioAqb7erdO6C-THcZe!BO7Mo|VHVwQUthA5; z4OcP}oB8g-CH|8h++8>gxJ+WIFhjaxy15>JQ2H#b823zy`l;S^{ zKVyimiLh9hNhvxHrz7rrjb(=++fY7r-n~CTBhuJfRc-%kCt{5pIa zbY;RkN^wO>FfJz(dT^ccIP}0~?O-*>Tx@F_9XSRJ165w1=W9E1?}@n(->0#WHS~Hh z+kxdZ!_AzRxZsIgoyOFnJDASZ?;Z!y;e|Wu6v&RI_Y{z}v{{ih<2a24^xC}F2{tHA zIKoO!xUDPOw|a5Iltb+s?Q<_hGuYReCIECU0{K^Pv368`p>O%U4U;~;WZg-O0ehhK zc#*a?CDN>9nNHfdjUNo6_9;^S+UjIIQE8pUdgKx&%Hi#nr{$@d?WaDdsvX+a=f-Kc zG81Fi4`m z(BPOaABUira^yBF1uW-{wK4TMliIkEQG8^MF-@wQXsP&Tn577zgxBKH!h~9&iD`}J z_E zGZ6jw(2m8<&ED9B%h1Khmfo7)n$6kT$d$p?+GG#mz~RFdXkA}VZ}s?_?E{F3M?!R+ z9Z+Nsm35lI6=^aUq8?*9fGAznai|>0blfmz_seMOYavEl(K@>1W88H{`Ccdy$nyu} z7r!3l$q%FedIsPS7-0W>n+#wK7Wy*$4WK`N8u2ZqZK^aOCm}^L_i>2mYCJ-St&|4{7YbEaZOXp0xsttLHrRD?&JSk5FY?f{2l+Z>-Pthdlvccee}2Uizfev r)KBOC(`)|NO!wXY?=>|14gAkwATJI5edl6>VS_aRRe}t~9gY77rB6Ql literal 0 HcmV?d00001 From 764a1a48cd79dcb464c929cb3cef0ea0f1afb623 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:29:55 +0200 Subject: [PATCH 18/36] test: retain actual queued codebook browser download --- scripts/fixtures/word-acceptance/README.md | 4 ++++ .../word-acceptance/kodierbox-browser.docx | Bin 0 -> 10827 bytes 2 files changed, 4 insertions(+) create mode 100644 scripts/fixtures/word-acceptance/kodierbox-browser.docx diff --git a/scripts/fixtures/word-acceptance/README.md b/scripts/fixtures/word-acceptance/README.md index c7df3b6..ecfb2b7 100644 --- a/scripts/fixtures/word-acceptance/README.md +++ b/scripts/fixtures/word-acceptance/README.md @@ -28,3 +28,7 @@ and dependency versions. They support direct same-engine visual comparison. and mixed formula/list/image cases. Regenerate after `npm run build_cc` with `node scripts/create-codebook-editor-office-fixture.mjs`. Its executable contract is `scripts/codebook-editor-matrix.test.mjs` (384 cases, each ESM and CommonJS). + +`kodierbox-browser.docx` is the corresponding actual browser download produced +by a real Redis generation job in Kodierbox. Its JSON companion and every +normalized DOCX part match the Studio browser download exactly. diff --git a/scripts/fixtures/word-acceptance/kodierbox-browser.docx b/scripts/fixtures/word-acceptance/kodierbox-browser.docx new file mode 100644 index 0000000000000000000000000000000000000000..ebefffd48d940443aa78efbb933dab5ee24d6425 GIT binary patch literal 10827 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 literal 0 HcmV?d00001 From 2617b45eb133d3018205ea093ee610091bfc9b82 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Tue, 13 Oct 2026 15:35:17 +0200 Subject: [PATCH 19/36] docs: record Office and host browser acceptance results --- docs/codebook-acceptance-2026-10-13.md | 120 +++++++++++++++++++++++++ docs/shared-codebook.md | 16 +++- 2 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 docs/codebook-acceptance-2026-10-13.md diff --git a/docs/codebook-acceptance-2026-10-13.md b/docs/codebook-acceptance-2026-10-13.md new file mode 100644 index 0000000..d04ad64 --- /dev/null +++ b/docs/codebook-acceptance-2026-10-13.md @@ -0,0 +1,120 @@ +# Codebook-Abnahme vom 13.10.2026 + +Die P2-Regression bei geschützten und wiederholten Leerzeichen ist behoben. +Die bisherige Studio-Ausgabe bleibt in den stabilen Referenzfällen erhalten; +die direkten Studio- und die asynchronen Kodierbox-Exporte liefern identische Dokumentinhalte. + +## Automatisierte Nachweise + +| Prüfung | Ergebnis | +| --- | --- | +| Generator | 1.206 Tests bestanden, keine übersprungen | +| Neue Editor-Matrix | 384 Kombinationen, jeweils ESM und CommonJS, allgemeine und Code-Instruktionen | +| Ursprüngliches Studio | 32 bestehende + 20 neue unabhängige JSON-/DOCX-Referenzfälle | +| Installierte Hosts | 4.654 Exportpaare: JSON und alle relevanten DOCX-Bestandteile identisch | +| Studio | 49 API- und 66 Frontendtests bestanden | +| Kodierbox | 58 Backend- und 60 Frontendtests bestanden | +| Builds | Bibliothek und alle vier Host-Builds erfolgreich | +| Lint | Keine Fehler; Bibliothek 18, Studio API 6 bestehende Warnungen | + +Alle 20 neuen Leerzeichenfälle waren vor dem Fix rot und danach grün. +Die vollständigen Host-Suiten wurden für diesen Fix nicht erneut ausgeführt. +Referenz: ursprünglicher Studio-Commit `2504265721a59c7c65425072951ce82ab16f0fb8`. +Die Laufzeitänderungen wurden mit Bibliothek `aa404bc2`, Studio `b12fd461` +und Kodierbox `92b88f30` geprüft; spätere Ergänzungen betreffen Tests, synthetische +Abnahmedokumente und Dokumentation. + +Paket: identischer lokal gepackter 4.1.0-Tarball in beiden Anwendungen. +SHA256: `dcee6cc39436baf91cf2558afedfbc202e044ca14dbf72c7bfdf4b381e5c0ccd`. +Alle installierten Paketdateien wurden mit dem Tarball verglichen; beide +Root-Locks und Studios API-Runtime-Lock enthalten dessen neue Integrität. + +## Echte Browserabläufe + +Beide gebauten Frontends wurden über einen lokalen Static-/API-Proxy mit ihren +unveränderten produktiven NestJS-Bundles ausgeführt. Die PostgreSQL-Datenbanken +wurden mit den vollständigen Repository-Migrationen frisch angelegt. Studio +verwendete Liquibase 4.33, Kodierbox die vorgesehene Version 4.28. Kodierbox nutzte +echtes Redis/Bull und einen isolierten Keycloak mit zwei synthetischen Identitäten. +Die Fixture-Einrichtung erfolgte über echte APIs; Kodierbox-VOCS und die +Jobdefinition wurden als erfundene Datensätze direkt in die Testdatenbank gesetzt. +Die erfolgreichen Exportabläufe enthielten keine API-/Auth-/Queue-Mocks. + +- **Studio:** JSON-/DOCX-Download, leere Auswahl gesperrt, Suche erhält Gesamtauswahl, + Dialog schließt nach Download, anonymer Zugriff 401, fremder Workspace 403, + Missing-Code 0 über reales Profil, Score-Abwahl und Schulungsfilter geprüft. +- **Kodierbox:** JSON-/DOCX über echte Jobanlage, Statusabfrage und Download; + Jobdefinitionspicker beschränkt auf A/ALIAS_00, ausgewähltes Missingprofil und + Score-Abwahl stimmen mit dem Download überein. Doppelauslösung ist gesperrt; + erneuter Versuch und Ende des Pollings beim Schließen funktionieren. +- **Fehlerzweige:** Studio-Exportfehler sowie Kodierbox-Anlage-, Status- und + Downloadfehler wurden gezielt im Browsernetzwerk injiziert. Meldung und + Freigabe für erneuten Versuch wurden geprüft; erfolgreicher Retry lief wieder + gegen die echte Queue. +- **Reale Zugriffsprüfungen:** Kodierbox weist anonyme und nicht zugewiesene Nutzer + ab (bestehender WorkspaceGuard: 401), fremde Job-Workspaces mit 403, fremde + ausgewählte Einheiten mit 404 und ungültiges Format/Boolean mit 400. Ein + erfolgreicher Download räumt sein Ergebnis auf; Folgeabruf liefert 404. +- Beide erfolgreichen Browser-Downloads sind in JSON und allen normalisierten + DOCX-Bestandteilen identisch. NBSP, schmales NBSP und doppelte Abstände bleiben + erhalten. Keine unbehandelten Browser-Ausnahmen in den bestandenen Abläufen. + +Testumgebung: PostgreSQL 16.11, Redis 7.4, Keycloak 26.7.3. Das ist keine +Produktions- oder Deployment-Abnahme; Kodierboxs PostgreSQL-14-Produktionsversion +wurde damit nicht separat geprüft. Der vorhandene Keycloak-JS-Adapter 23.0.7 +benötigte im Testrealm den dokumentierten Nonce-Kompatibilitätsmapper +([Keycloak Upgrade Guide](https://www.keycloak.org/docs/26.7.5/upgrading/index.html)). +Alle isolierten Testdienste wurden anschließend beendet und entfernt. + +## Microsoft Office + +Die tatsächlichen DOCX-Dateien wurden von **Microsoft Office Web Viewer / Word** +gerendert, ohne Office-Anmeldung. Es wurden ausschließlich erfundene, im +Bibliotheksbranch versionierte Testdokumente verwendet. + +| Dokument | Seiten | +| --- | ---: | +| Aktuelles Rich-Text-Dokument mit Scores | 3 | +| Ursprüngliches Studio mit Scores | 3 | +| Aktuelles Rich-Text-Dokument ohne Scores | 3 | +| Ursprüngliches Studio ohne Scores | 3 | +| Tatsächlicher Studio-Browser-Download | 3 | +| Tatsächlicher Kodierbox-Browser-Download | 3 | +| 48 Markierungskombinationen plus Formel/Listen/Bild | 2 | +| **Gesamt** | **20** | + +Tabellenspalten, Umbrüche, Bilder, Formeln, Markierungen und Fußzeilen/Seitenzahlen +wurden anhand der gerenderten Seiten geprüft. Alle sechs Seiteninhalte der +aktuellen/ursprünglichen Studio-Paare sind pixelgleich (Seitenbereich oberhalb +der Fußzeile; die vereinbarte Umbenennung zu „IQB Codebook“ wird separat geprüft). +Die beiden echten Browser-Dokumente sind auf allen drei vollständigen Seiten +pixelgleich. Es traten keine sichtbaren abgeschnittenen Inhalte oder defekten +Formeln/Bilder auf. Dies ist ein echter Microsoft-Word-Web-Nachweis; Windows- +oder macOS-Desktop-Word wurde nicht ausgeführt. + +## Aspect-Abgleich und Grenzen + +Quellabgleich mit `iqb-berlin/verona-modules-aspect@fb3f5982`: Die 384 Tests +umfassen alle Kombinationen der gemeinsamen vier Textmarkierungen, drei gültigen +Grundlinienzustände, zwei Verschachtelungsreihenfolgen und vier Ausrichtungen. +Der Codebook-Vertrag liest Kodierinstruktionen aus VOCS, nicht vollständige +Aspect-Aufgaben mit interaktiven Player-Elementen. Aspect-Formelknoten verwenden +einen anderen Vertrag als die IQB-Formeln des Instruktionseditors. Ein kompletter +Aspect-Editor-/Player-Browserlauf wurde nicht durchgeführt. + +Die bestehenden Studio-Grenzen bei CSS-Listenstilen, verschachtelten +Listeneinrückungen, Bild-Floats sowie innerhalb eines Absatzes wechselnder Farbe/ +Schriftgröße bleiben dokumentiert. Eine uneingeschränkte HTML-/CSS-Layouttreue +oder Unterstützung aller Aspect-Player-Knoten wird nicht behauptet. + +## Veröffentlichung und CI + +Alle drei Branches `shared-codebook` wurden committed und gepusht. Die offenen +PRs #239 (Bibliothek) und #1041 (Kodierbox) werden mit diesen Ergebnissen aktualisiert. +Studio-PR #1728 ist ohne Merge geschlossen; der Studio-Branch wurde aktualisiert. +Bibliotheks-CI war für `2925485` vollständig grün. Die CI der abschließenden +Dokumentationscommits wird separat angegeben. Kodierboxs zuletzt geprüfte +GitLab-Pipeline [103668](https://scm.cms.hu-berlin.de/iqb/coding-box/-/pipelines/103668) +für `92b88f30` ist fehlgeschlagen; die Jobursache bleibt hinter Bot-Schutz/ +Anmeldung unbestätigt. Lokale grüne Tests ersetzen diesen offenen CI-Status nicht. +Kein Merge, Release, Paket-Publish oder Rollout wurde ausgeführt. diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md index ce5aca1..e625886 100644 --- a/docs/shared-codebook.md +++ b/docs/shared-codebook.md @@ -108,10 +108,18 @@ Rollback: vorherige Bibliotheks-/Anwendungsversion; keine Datenmigration. * Vor Ausrollen bleiben visuelle Mehrseiten-DOCX-Abnahme und Host-Browserabläufe einschließlich Netzwerkprüfung verpflichtend. -### Microsoft Word unter Windows - -Word unter Windows ist ein eigener Abnahmeweg. XML-Gleichheit und LibreOffice- -Rendering ersetzen ihn nicht. Der Installationsvergleich legt künstliche Exporte +### Microsoft Office + +Microsoft Office Web Viewer / Word hat am 13.10.2026 sieben ausschließlich +synthetische Dokumente mit insgesamt 20 Seiten gerendert. Die Host-Browser- +Downloads sind auf allen drei vollständigen Seiten pixelgleich. Die alten und +neuen Studio-Rich-Text-Dokumente sind mit/ohne Scores in ihren sechs +Seiteninhalten pixelgleich; die vereinbarte Fußzeile wird separat geprüft. +Details und Grenzen: [Abnahmeprotokoll](./codebook-acceptance-2026-10-13.md). + +XML-Gleichheit und LibreOffice-Rendering allein ersetzen keinen Office-Nachweis. +Für einen zusätzlichen Windows-Desktop-Word-Abnahmeweg legt der +Installationsvergleich künstliche Exporte sechs DOCX-Dateien und `word-expectations.json` im Ausgabeverzeichnis ab: je Anwendung einen Filter-/Missing-0-Fall sowie Rich-Text-Fälle mit und ohne Score-Spalte. Die Rich-Text-Fälle werden zusätzlich direkt gegen die alte Studio-Referenz geprüft. From 1b821b7659df306c7a18de8b3ff5c9301b8d6568 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:03:11 +0200 Subject: [PATCH 20/36] test: capture browser-authored formatting regression document --- .../format-boundaries-before.docx | Bin 0 -> 8757 bytes .../format-boundaries-source.json | 54 ++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 scripts/fixtures/word-acceptance/format-boundaries-before.docx create mode 100644 scripts/fixtures/word-acceptance/format-boundaries-source.json diff --git a/scripts/fixtures/word-acceptance/format-boundaries-before.docx b/scripts/fixtures/word-acceptance/format-boundaries-before.docx new file mode 100644 index 0000000000000000000000000000000000000000..95693774799480b66a4177de5f6c293ca2c8d29d GIT binary patch literal 8757 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 ROT Schwarz GRUEN Ende

    H1

    H2

    H3

    H4

    1. LISTE

    EINRUECKUNG

    ", + "styles": [ + { + "tag": "P", + "text": "Vor ROT Schwarz GRUEN Ende", + "font": "20px", + "indent": "0px", + "align": "start" + }, + { + "tag": "H1", + "text": "H1", + "font": "20px", + "indent": "0px", + "align": "start" + }, + { + "tag": "H2", + "text": "H2", + "font": "18px", + "indent": "0px", + "align": "start" + }, + { + "tag": "H3", + "text": "H3", + "font": "16px", + "indent": "0px", + "align": "start" + }, + { + "tag": "H4", + "text": "H4", + "font": "16px", + "indent": "0px", + "align": "start" + }, + { + "tag": "OL", + "text": "LISTE", + "font": "20px", + "indent": "40px", + "align": "start" + }, + { + "tag": "P", + "text": "EINRUECKUNG", + "font": "20px", + "indent": "40px", + "align": "start" + } + ] +} \ No newline at end of file From 0607ba5d34b6c932dec30aa65a0287665d1d80e1 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:26:39 +0200 Subject: [PATCH 21/36] test: add synthetic editor and player Word acceptance matrices --- .../aspect-feature-specimen.docx | Bin 0 -> 20032 bytes .../word-acceptance/aspect-full-matrix.docx | Bin 0 -> 79795 bytes .../word-acceptance/cc-feature-specimen.docx | Bin 0 -> 19627 bytes .../word-acceptance/cc-full-matrix.docx | Bin 0 -> 79364 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 scripts/fixtures/word-acceptance/aspect-feature-specimen.docx create mode 100644 scripts/fixtures/word-acceptance/aspect-full-matrix.docx create mode 100644 scripts/fixtures/word-acceptance/cc-feature-specimen.docx create mode 100644 scripts/fixtures/word-acceptance/cc-full-matrix.docx diff --git a/scripts/fixtures/word-acceptance/aspect-feature-specimen.docx b/scripts/fixtures/word-acceptance/aspect-feature-specimen.docx new file mode 100644 index 0000000000000000000000000000000000000000..1d7e83a497f1083b9fb8dad2e190cc3108b99631 GIT binary patch literal 20032 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 Q!hykoDFFXD2tt|rKls8X$p8QV literal 0 HcmV?d00001 diff --git a/scripts/fixtures/word-acceptance/aspect-full-matrix.docx b/scripts/fixtures/word-acceptance/aspect-full-matrix.docx new file mode 100644 index 0000000000000000000000000000000000000000..6d6e74abd03db706da317fa1cfa8738d28195652 GIT binary patch literal 79795 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�ryf=#$A)^Py|yjm*43%ZI1$RE>Kz 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 {_@XIxSfG6@6e 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 PG*YxY@Y^!rLqq!?4lj_H literal 0 HcmV?d00001 diff --git a/scripts/fixtures/word-acceptance/cc-feature-specimen.docx b/scripts/fixtures/word-acceptance/cc-feature-specimen.docx new file mode 100644 index 0000000000000000000000000000000000000000..a441f0a671185050a9b520f9b8f77e802041f0e4 GIT binary patch literal 19627 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 AyZ`_I literal 0 HcmV?d00001 diff --git a/scripts/fixtures/word-acceptance/cc-full-matrix.docx b/scripts/fixtures/word-acceptance/cc-full-matrix.docx new file mode 100644 index 0000000000000000000000000000000000000000..794d2bf44e01d40e76455afa9a7480e4170dbbc6 GIT binary patch literal 79364 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∨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 Date: Tue, 13 Oct 2026 20:54:18 +0200 Subject: [PATCH 22/36] test: capture real toolbar and styled formula Word exports --- .../studio-reference-current.docx | Bin 0 -> 10755 bytes .../word-acceptance/toolbar-acceptance.docx | Bin 0 -> 17067 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 scripts/fixtures/word-acceptance/studio-reference-current.docx create mode 100644 scripts/fixtures/word-acceptance/toolbar-acceptance.docx diff --git a/scripts/fixtures/word-acceptance/studio-reference-current.docx b/scripts/fixtures/word-acceptance/studio-reference-current.docx new file mode 100644 index 0000000000000000000000000000000000000000..32a8f90745267ce00fafe2bb50c2ff4e10466b8f GIT binary patch literal 10755 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 e!<)Z>{}~Jvq@li_Tr4mwux3z|z-@_X}mp3vj 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 s@qd@t`M1P>g%1CBiOQUR>G-b`zqA Date: Tue, 13 Oct 2026 21:28:58 +0200 Subject: [PATCH 23/36] test: capture compound formula rendering before correction --- .../word-acceptance/formula-compound-before.docx | Bin 0 -> 9297 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 scripts/fixtures/word-acceptance/formula-compound-before.docx diff --git a/scripts/fixtures/word-acceptance/formula-compound-before.docx b/scripts/fixtures/word-acceptance/formula-compound-before.docx new file mode 100644 index 0000000000000000000000000000000000000000..d25846e13bcec99fef0c86974e70d33825af08b7 GIT binary patch literal 9297 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 W9D(rtbHM}P0a&5z_Ac4Y%l{u9i+cG0 literal 0 HcmV?d00001 From d3754d6a86dcfff6f4afbc92f037fb53c0586b64 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:35:47 +0200 Subject: [PATCH 24/36] test: capture corrected native compound math in Word --- .../word-acceptance/formula-compound-fixed.docx | Bin 0 -> 9379 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 scripts/fixtures/word-acceptance/formula-compound-fixed.docx diff --git a/scripts/fixtures/word-acceptance/formula-compound-fixed.docx b/scripts/fixtures/word-acceptance/formula-compound-fixed.docx new file mode 100644 index 0000000000000000000000000000000000000000..81f723d218a791d84d6d8a341d9f32793964f685 GIT binary patch literal 9379 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 Date: Tue, 13 Oct 2026 21:39:32 +0200 Subject: [PATCH 25/36] test: refresh Word matrices with native math corrections --- .../aspect-feature-specimen.docx | Bin 20032 -> 20034 bytes .../word-acceptance/aspect-full-matrix.docx | Bin 79795 -> 79797 bytes .../word-acceptance/cc-feature-specimen.docx | Bin 19627 -> 19623 bytes .../word-acceptance/cc-full-matrix.docx | Bin 79364 -> 79366 bytes .../word-acceptance/toolbar-acceptance.docx | Bin 17067 -> 17146 bytes 5 files changed, 0 insertions(+), 0 deletions(-) diff --git a/scripts/fixtures/word-acceptance/aspect-feature-specimen.docx b/scripts/fixtures/word-acceptance/aspect-feature-specimen.docx index 1d7e83a497f1083b9fb8dad2e190cc3108b99631..8ed548be50e5750e7602a63bb7e5c2fc923c5ea8 100644 GIT binary patch delta 12541 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 delta 12525 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 diff --git a/scripts/fixtures/word-acceptance/cc-feature-specimen.docx b/scripts/fixtures/word-acceptance/cc-feature-specimen.docx index a441f0a671185050a9b520f9b8f77e802041f0e4..7d07d4c8e3ac55c15cd627896538e5d12e3e930c 100644 GIT binary patch delta 12151 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 T`)}9(t+S5`BzRm>80h~G`qPjQ delta 12329 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 WzGzPz7XcrH58?oh5|H4YA^!*HFoF31 delta 3863 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 diff --git a/scripts/fixtures/word-acceptance/toolbar-acceptance.docx b/scripts/fixtures/word-acceptance/toolbar-acceptance.docx index e8c6d40103890771ee9f5695fda82d0d52b2ed53..5a303722401cf3b3a6854e9188383106deac284c 100644 GIT binary patch delta 9935 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 R{(F_q-y_4=0)a?K{|C^+0J8u9 From 351dec0e23181bf5c943877a99ebfb9e1b125da2 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:05:21 +0200 Subject: [PATCH 26/36] fix: preserve rich text formatting through codebook exports --- docs/codebook-acceptance-2026-10-13.md | 2 + docs/codebook-aspect-acceptance-2026-10-13.md | 82 ++++ docs/shared-codebook.md | 4 + package.json | 2 +- .../codebook-docx-generator.ts | 457 ++++++++++++++---- .../codebook-generator/manual-instruction.ts | 3 +- .../extensions/hanging-indent.ts | 15 +- .../extensions/indent-selection.spec.ts | 80 +++ .../lib/rich-text-editor/extensions/indent.ts | 6 +- .../extensions/iqb-math-formula.spec.ts | 31 ++ .../extensions/iqb-math-formula.ts | 4 +- .../extensions/paragraph-extension.ts | 17 +- .../extensions/preserve-whitespace.spec.ts | 45 ++ .../extensions/preserve-whitespace.ts | 15 + .../rich-text-editor.component.html | 2 +- .../rich-text-editor.component.ts | 4 +- scripts/codebook-contract.test.mjs | 6 +- scripts/codebook-docx-test-utils.mjs | 25 +- scripts/codebook-docx-test-utils.test.mjs | 20 + scripts/codebook-matrix.test.mjs | 4 +- scripts/codebook-rich-layout.test.mjs | 203 ++++++++ scripts/codebook-rich-matrix.test.mjs | 111 +++++ scripts/compare-codebook-installations.mjs | 4 +- .../fixtures/codebook-rich-format-cases.mjs | 118 +++++ scripts/fixtures/word-acceptance/README.md | 8 + 25 files changed, 1129 insertions(+), 139 deletions(-) create mode 100644 docs/codebook-aspect-acceptance-2026-10-13.md create mode 100644 projects/ngx-coding-components/src/lib/rich-text-editor/extensions/indent-selection.spec.ts create mode 100644 projects/ngx-coding-components/src/lib/rich-text-editor/extensions/iqb-math-formula.spec.ts create mode 100644 projects/ngx-coding-components/src/lib/rich-text-editor/extensions/preserve-whitespace.spec.ts create mode 100644 projects/ngx-coding-components/src/lib/rich-text-editor/extensions/preserve-whitespace.ts create mode 100644 scripts/codebook-docx-test-utils.test.mjs create mode 100644 scripts/codebook-rich-layout.test.mjs create mode 100644 scripts/codebook-rich-matrix.test.mjs create mode 100644 scripts/fixtures/codebook-rich-format-cases.mjs diff --git a/docs/codebook-acceptance-2026-10-13.md b/docs/codebook-acceptance-2026-10-13.md index d04ad64..b0cd8e8 100644 --- a/docs/codebook-acceptance-2026-10-13.md +++ b/docs/codebook-acceptance-2026-10-13.md @@ -1,5 +1,7 @@ # Codebook-Abnahme vom 13.10.2026 +Dieser Bericht beschreibt den früheren Paketstand `dcee6cc…`. Die aktuelle Formatkorrektur und Aspect-Abnahme steht im [ergänzenden Bericht](codebook-aspect-acceptance-2026-10-13.md). + Die P2-Regression bei geschützten und wiederholten Leerzeichen ist behoben. Die bisherige Studio-Ausgabe bleibt in den stabilen Referenzfällen erhalten; die direkten Studio- und die asynchronen Kodierbox-Exporte liefern identische Dokumentinhalte. diff --git a/docs/codebook-aspect-acceptance-2026-10-13.md b/docs/codebook-aspect-acceptance-2026-10-13.md new file mode 100644 index 0000000..3b6ff65 --- /dev/null +++ b/docs/codebook-aspect-acceptance-2026-10-13.md @@ -0,0 +1,82 @@ +# Browser- und Word-Abnahme der Codebooks + +Die bestätigten Formatierungsfehler sind behoben. Der echte Aspect Editor und Player, der gemeinsame Instruktionseditor sowie Studio und Kodierbox wurden mit synthetischen Daten geprüft. Microsoft Office Web Viewer / Word hat 262 Seiten gerendert; die Aufnahmen jeder Seite sind mit deren tatsächlicher Fußzeile abgeglichen. + +## Umfang und Ergebnisse + +| Prüfung | Ergebnis | +| --- | --- | +| Aspect: HTML laden, Unit über die Oberfläche speichern, echter Verona-Player | 2.740 Formatfälle, 40 native Formeln | +| Gemeinsamer Editor: HTML laden, speichern und wiederöffnen | 2.738 unterstützte Formatfälle | +| Vollständige gültige Textmarkierungsmatrix je Editor | 2.496 = 16 Kombinationen aus fett/kursiv/unterstrichen/durchgestrichen × normal/tief/hoch × vier Ausrichtungen × 13 Schriftgrößen | +| Weitere Matrixfälle | 16 Farbgrenzen, neun Listenstile auf fünf Ebenen, 120 Absatzkombinationen, 16 Überschriften, Bildpositionen, Formeln, Sonderzeichen, Umbrüche und Zitate; Aspect zusätzlich Inlinebild und Trennlinien | +| Tatsächliche Toolbar-Bedienung | 57 Aspect- und 52 gemeinsame Editorfälle; jeweils alle 38 Sonderzeichen-Schaltflächen; gemeinsamer Formeldialog zusätzlich separat geprüft | +| HTML-Austausch in beiden Richtungen | Je 2.738 Fälle und 40 Formeln erhalten; Speichern/Wiederöffnen und echter Player erfolgreich | +| Austausch bis Word | In beiden Richtungen **alle normalisierten DOCX-Bestandteile identisch** zur tatsächlich in Word gerenderten Matrix | +| Generator | 1.245 Tests bestanden, keine übersprungen; ESM und CommonJS, mit und ohne Scores | +| Editor-Regressionsprüfungen | Gemeinsamer Editor 10 Tests; Aspect 16 Tests bestanden | +| Installationsvergleich | 4.654 Studio-/Kodierbox-Exportpaare: JSON und alle normalisierten DOCX-Bestandteile identisch | +| Host-Tests | Studio 49 API + 66 Frontend; Kodierbox 58 Backend + 60 Frontend bestanden | +| Builds | Bibliothek, frischer Aspect Editor/Player und alle vier Host-Builds erfolgreich | +| Installation | Identischer 4.1.0-Tarball in beiden Hosts; zusätzlich saubere Studio-Runtime-Installation mit 515 Paketen; alle 20 Paketdateien in allen drei Installationen bytegleich | + +Die große Matrix läuft über die tatsächlichen Tiptap-Parser und Serialisierungen. Die Bedienfälle prüfen zusätzlich echte Schaltflächen, Auswahllisten, Bilddateiauswahl und Speicherdialoge. Der gemeinsame MathLive-Dialog wird über seinen öffentlichen Eingabewert und das reale Input-Ereignis bedient. Die erfolgreichen Editor-, Player-, Authentifizierungs-, API- und Queue-Abläufe verwenden ihre echten Implementierungen. + +## Behobene Fehler + +- Normale Mehrfachabstände wurden beim Laden und Wiederöffnen reduziert. Normale, geschützte und schmale geschützte Leerzeichen bleiben erhalten; JSON-Inhalte und ausdrücklich gesetzte Parseroptionen funktionieren weiter. +- Farben, Schriftgrößen und Hervorhebungen wurden auf den ganzen Absatz ausgedehnt. Sie gelten jetzt nur für die jeweilige markierte Textstelle. CSS-Pixel und Punktangaben werden korrekt in Word-Halbpunkte umgerechnet. +- Absatzabstände, Einzüge, hängende Einzüge und formatierte Listen gingen im Word-Export verloren. Die gesetzten Attribute und Listenmarker bleiben erhalten. +- Einrückung und hängender Einzug funktionierten nach „Alles auswählen“ nicht; im gemeinsamen Editor scheiterte Absatzabstand an Textknoten. Beide Bedienpfade sind repariert. +- Inlinebilder wurden aus dem Text herausgezogen oder doppelt ausgegeben; gesetzte linke/rechte Bildpositionen gingen verloren. Word erhält die Platzierung einschließlich Textumlauf-Ankern. +- Aspect-Formeln wurden aus veraltetem MathLive-Anzeige-HTML gelesen; reine Formelinstruktionen konnten herausgefiltert werden. Beide Formel-HTML-Formate werden aus ihrem LaTeX verarbeitet, und der Instruktionsfilter erkennt reine Aspect-Formeln. +- Die Editoren erkannten beim HTML-Austausch jeweils nur ihren eigenen Formelknoten. Beide lesen nun beide Formate und speichern im eigenen kanonischen Format weiter. +- Über- und Unterstriche wurden als kurzer Akzent und teils an der falschen Position dargestellt. Native Word-Balken umfassen jetzt den ganzen Ausdruck. Bruchstriche, Wurzelzeichen und große Operatoren erhalten dieselbe Farbe und Größe wie die Formelzeichen; der Vorher-/Nachher-Nachweis wurde in Word tatsächlich geprüft. +- Schriftgröße, Farbe und Textmarkierungen um Formeln gingen im nativen Word-Mathematikformat verloren. Die umgebenden Eigenschaften werden auf native Mathematikläufe übertragen; der Formelcache bleibt unverändert und überträgt keine fremde Formatierung. +- Formelabsätze wurden immer links ausgerichtet; die gewählte Ausrichtung bleibt erhalten. Zeilenumbrüche innerhalb von LaTeX-Attributen werden nicht mehr in HTML-Break-Tags umgewandelt. +- Aspect erlaubte keine erneute Auswahl derselben Bilddatei. Das Dateifeld wird nach dem Auslesen geleert; die vier Bildpositionen wurden mit derselben Datei tatsächlich eingefügt. + +## Erhalt der bisherigen Studio-Ausgabe + +Die unabhängigen Referenzen stammen weiterhin vom ursprünglichen Studio-Commit `2504265721a59c7c65425072951ce82ab16f0fb8`. Die 32 bestehenden Rohschema-Referenzen und 20 Leerzeichenfälle wurden nicht aus der neuen Implementierung neu erzeugt. + +Grundlayout, Auswahlregeln, Scores, Missing-Codes, Beziehungen, Bilder und unveränderte Formatfälle bleiben gegen die alten vollständigen JSON-/DOCX-Referenzen abgesichert. Eine **bewusste Korrektur** betrifft den alten `Farbe`-Referenzabsatz: explizite `18pt` wurden früher als 18 Word-Halbpunkte, also 9pt, behandelt; nun werden korrekt 36 Halbpunkte ausgegeben. Der Vergleich normalisiert ausschließlich diesen streng geprüften Unterschied und die gleichwertige Hex-Schreibweise der Hervorhebung zurück. Alle anderen Dokumentbestandteile bleiben Gegenstand des ursprünglichen Hashvergleichs. Neue Tests prüfen die korrigierten Werte unmittelbar. + +Die frühere pixelgleiche Abnahme des Pakets `dcee6cc…` ist ein historischer Nachweis. Für das jetzige Paket gelten die oben beschriebenen gezielten Darstellungsfixes und die neue Word-Abnahme; eine pauschale Pixelgleichheit zu allen alten Fehlformatierungen wird nicht behauptet. + +## Microsoft Word + +| Dokument | Seiten | +| --- | ---: | +| Aspect: vollständige Formatmatrix | 114 | +| Gemeinsamer Editor: vollständige Formatmatrix | 114 | +| Aspect: Übersicht aller unterschiedlichen Formatmerkmale | 12 | +| Gemeinsamer Editor: Formatübersicht | 12 | +| Tatsächlich bediente Toolbarfälle und 13 kombinierte Formelschriftgrößen | 6 | +| Aktuelle Studio-Referenz mit korrigierter expliziter Schriftgröße | 3 | +| Farbige zusammengesetzte Formeln, breite Über-/Unterstriche | 1 | +| **Gesamt** | **262** | + +Alle Seiten wurden aus dem echten Microsoft-Viewer erfasst. Dessen wiederverwendete Bildkacheln wurden anhand ihrer Seitenposition erfasst; ein zusätzlicher OCR-Abgleich bestätigt die erwartete Seitennummer jeder Aufnahme. Die Formatübersichten und repräsentative Matrixseiten wurden visuell geprüft. Die vollständigen Matrixinhalte, Markierungen, Größen und Ausrichtungen sind zusätzlich im DOCX geprüft. Beide Richtungen des Editor-Austauschs ergeben exakt dieselben Word-Dokumentbestandteile wie die gerenderte gemeinsame Matrix. + +Die Galerie enthält sämtliche Seiten in Originalauflösung. Verwendet wurden ausschließlich erfundene, versionierte Abnahmedokumente; kein Office-Konto und keine Nutzerdaten waren nötig. Dies ist eine Microsoft-Word-Web-Abnahme. Desktop-Word unter Windows oder macOS wurde nicht ausgeführt. + +## Echte Host-Downloads + +Studio und Kodierbox exportieren drei synthetische Aufgaben mit insgesamt 5.479 Code-Instruktionen: beide vollständigen Editor-Matrizen und eine kombinierte formatierte Aspect-Formel. Beide gebauten Frontends laufen mit den produktiven NestJS-Bundles. Studio verwendet eine frisch migrierte PostgreSQL-Datenbank; Kodierbox zusätzlich echtes Redis/Bull und Keycloak. + +Studio prüft JSON/DOCX, leere Auswahl, Auswahl während der Suche, Dialogabschluss, 401/403 sowie sichtbare Fehler und erneuten Versuch. Kodierbox prüft echte Jobanlage, Status und Download, doppelte Auslösung, Anlage-/Status-/Downloadfehler, erneuten Versuch und das Ende des Pollings beim Schließen. Ausschließlich Fehlerzweige werden gezielt im Browsernetzwerk injiziert. Erfolgreiche Downloads sind in JSON und jedem normalisierten DOCX-Bestandteil identisch; geschützte und wiederholte Leerzeichen bleiben erhalten. + +Testdienste: PostgreSQL 16.11 mit Repository-Migrationen, Studio Liquibase 4.33 und Kodierbox 4.28, Redis 7.4 und Keycloak 26.7.3. Die vorhandene Keycloak-JS-Version 23.0.7 benötigt im isolierten Testrealm den Nonce-Kompatibilitätsmapper. Die Prüfung ist lokal; sie bestätigt keine Produktion oder Bereitstellung. Die vollständigen Host-Testsuiten wurden nicht erneut ausgeführt. + +Der Dokumentvergleich normalisiert ausschließlich die Datumsausgabe in der Fußzeile, unabhängig vom Ausführungstag. Ein eigener Test stellt sicher, dass Datumsangaben und Markenbezeichnungen im eigentlichen Nutzerinhalt erhalten bleiben. Zusätzlich sind explizite Listenstarts bei 0 und 7 abgesichert. + +## Paket und Abgrenzung + +Paket-SHA256: `0e355c3896799ae765cb6e90b00147da0359c56f5fdc90d36b7caf40fbe59ab8`. + +Die Abnahme betrifft Rich Text in Textinhalten und manuellen Kodieranweisungen. Interaktive Antwortfelder sind eigenständige Player-Elemente. Farben wurden über repräsentative Hex-/RGB-/Namenswerte, Bildpositionen mit synthetischen PNG-Dateien geprüft. Formatierte Listen erhalten im statischen Word-Codebook explizite Marker; sie werden dort nicht automatisch neu nummeriert. + +Lokale Tests, Browser-/Word-Abnahme, Push und entfernte CI sind getrennte Nachweise. Der aktuelle CI-Stand wird bei den Pull Requests dokumentiert; aus lokalen Erfolgen wird keine grüne GitLab-Pipeline abgeleitet. + +Versionierte synthetische Word-Dateien: `scripts/fixtures/word-acceptance/` im coding-components-Repository. Reproduzierbare Generator- und Formatprüfungen: `npm run build_cc && npm run test:codebook`; Host-Vergleich: `scripts/compare-codebook-installations.mjs`. Die vollständige lokale Galerie und maschinenlesbaren Ausführungsnachweise ergänzen diese Zusammenfassung. diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md index e625886..ce6b390 100644 --- a/docs/shared-codebook.md +++ b/docs/shared-codebook.md @@ -1,5 +1,9 @@ # Gemeinsames Codebook: Integrationsvertrag +## Aktuelle Format- und Aspect-Abnahme + +Die [Browser- und Word-Abnahme vom 13.10.2026](codebook-aspect-acceptance-2026-10-13.md) ergänzt die frühere Leerzeichen-Abnahme um bestätigte Formatierungsfixes und den bidirektionalen Formel-Austausch. Aktuelles Paket-SHA256: `0e355c3896799ae765cb6e90b00147da0359c56f5fdc90d36b7caf40fbe59ab8`. 1.245 Generator-Tests, 4.654 installierte Exportpaare und 262 tatsächliche Microsoft-Word-Web-Seiten sind geprüft. Die explizite 18pt-Schriftgröße des alten `Farbe`-Referenzfalls wird bewusst korrigiert; alle übrigen alten Dokumentbestandteile bleiben streng verglichen. Angaben zu früheren Paketständen weiter unten sind historische Nachweise. + Studio ist die Referenz für Bedienung und Dokumentlayout. Die drei Einstiegspunkte `codebook-models`, `codebook-generator` und `codebook-export` trennen Daten, frameworkfreie Dokumenterstellung und Angular-Oberfläche. Die Bibliothek enthält diff --git a/package.json b/package.json index 973befa..df1e38b 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test:schemer:coverage": "ng test schemer --watch=false --browsers=ChromeHeadless --code-coverage", "test:coverage": "npm run test:cc:coverage && npm run test:schemer:coverage", "test:scheme": "node --test scripts/validate-coding-scheme.test.js", - "test:codebook": "node --test scripts/codebook-contract.test.mjs scripts/codebook-matrix.test.mjs scripts/codebook-whitespace.test.mjs scripts/codebook-editor-matrix.test.mjs", + "test:codebook": "node --test scripts/codebook-contract.test.mjs scripts/codebook-matrix.test.mjs scripts/codebook-whitespace.test.mjs scripts/codebook-editor-matrix.test.mjs scripts/codebook-rich-layout.test.mjs scripts/codebook-rich-matrix.test.mjs scripts/codebook-docx-test-utils.test.mjs", "test:schemer:responsive": "playwright test --config playwright.schemer.config.cjs" }, "dependencies": { diff --git a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts index 49fd647..e4b2b2d 100644 --- a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts +++ b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts @@ -4,6 +4,13 @@ import { HeadingLevel, ImageRun, NumberFormat, + HorizontalPositionAlign, + HorizontalPositionRelativeFrom, + VerticalPositionRelativeFrom, + TextWrappingType, + TextWrappingSide, + TabStopType, + Tab, Packer, Paragraph, Table, @@ -41,6 +48,14 @@ import { WebColors } from './web-colors'; // an `exports` map in that package would kill the boot -- see the swagger import in // setting.controller.ts. type FileChild = Paragraph | Table; +type InstructionParagraph = { + element: Element; + marker?: string; + listDepth?: number; + listIndent?: number; + quoteDepth?: number; + imageWidth?: number; +}; export class CodebookDocxGenerator { private static ommlCache: Record = {}; @@ -395,7 +410,8 @@ export class CodebookDocxGenerator { colorParsed: string, backgroundColor: string, size: string, - formatting: Pick = {} + formatting: Pick = {}, + imageWidth = 334 ): void { const runStyle = { ...formatting, @@ -406,15 +422,39 @@ export class CodebookDocxGenerator { nodes.forEach(node => { if (node.type === 'text') { // Preserve non-breaking spaces and repeated spacing from Studio instructions. - if (node.data) children.push(new TextRun({ text: node.data, ...runStyle })); + node.data.split('\n').forEach((line, index) => { + if (index) children.push(new TextRun({ break: 1, ...runStyle })); + if (line) children.push(new TextRun({ text: line, ...runStyle })); + }); return; } if (node.type !== 'tag') return; const element = node as Element; const tagName = element.name.toLowerCase(); - if (tagName === 'span' && element.attribs?.['class']?.split(/\s+/).includes('iqb-math-formula')) { - const latex = CodebookDocxGenerator.decodeLatex(element.attribs?.['data-latex'] || '').trim(); - if (latex) children.push(CodebookDocxGenerator.latexToOmml(latex) || new TextRun({ text: latex, ...runStyle })); + const styles = CodebookDocxGenerator.inlineStyles(element); + const nestedColor = CodebookDocxGenerator.cssColor(styles['color'], colorParsed); + const nestedBackground = CodebookDocxGenerator.cssColor(styles['background-color'], backgroundColor); + const nestedSize = styles['font-size'] || size; + if ((tagName === 'span' && element.attribs?.['class']?.split(/\s+/).includes('iqb-math-formula')) || + tagName === 'aspect-nodeview-math-formula') { + const storedLatex = element.attribs?.['data-latex'] || element.attribs?.['formula'] || ''; + const latex = CodebookDocxGenerator.decodeLatex(storedLatex).trim(); + if (latex) { + const mathStyle: IRunOptions = { + ...formatting, + size: CodebookDocxGenerator.getFontSize(nestedSize), + color: nestedColor, + shading: { fill: nestedBackground } + }; + children.push( + CodebookDocxGenerator.latexToOmml(latex, mathStyle) || new TextRun({ text: latex, ...mathStyle }) + ); + } + return; + } + if (tagName === 'img') { + const fontPixels = (CodebookDocxGenerator.getFontSize(size) * 2) / 3; + children.push(CodebookDocxGenerator.createImageRun(element, imageWidth, fontPixels)); return; } if (tagName === 'br') { @@ -429,7 +469,7 @@ export class CodebookDocxGenerator { if (tagName === 'sub') { nestedFormatting.subScript = true; nestedFormatting.superScript = false; } if (tagName === 'sup') { nestedFormatting.superScript = true; nestedFormatting.subScript = false; } CodebookDocxGenerator.processInlineElements( - element.children, children, colorParsed, backgroundColor, size, nestedFormatting + element.children, children, nestedColor, nestedBackground, nestedSize, nestedFormatting, imageWidth ); }); } @@ -465,11 +505,6 @@ export class CodebookDocxGenerator { elem: Element, contentSetting: CodeBookContentSetting ): Paragraph { - const imageBuffer = Buffer.from( - elem.attribs['src'].substring(elem.attribs['src'].indexOf(',') + 1), - 'base64' - ); - const size = CodebookDocxGenerator.getImageSize(imageBuffer); return new Paragraph({ spacing: { before: 100, @@ -480,56 +515,68 @@ export class CodebookDocxGenerator { end: 100 }, children: [ - new ImageRun({ - data: imageBuffer, - transformation: CodebookDocxGenerator.getTransformation( - size, - contentSetting.showScore ? 334 : 382 - ) - }) + CodebookDocxGenerator.createImageRun(elem, contentSetting.showScore ? 334 : 382) ] }); } - private static getBackgroundColor( - cheerioAPI: cheerio.CheerioAPI, - elem: Element - ): string { - const mark = cheerioAPI(elem).find('mark'); - return cheerioAPI(mark).css('background-color') || '#FFFFFF'; + private static createImageRun(elem: Element, maxWidth: number, fontPixels = 20): ImageRun { + const source = elem.attribs['src'] || ''; + const imageBuffer = Buffer.from(source.substring(source.indexOf(',') + 1), 'base64'); + const actual = CodebookDocxGenerator.getImageSize(imageBuffer); + const styles = CodebookDocxGenerator.inlineStyles(elem); + let height = actual.height; + if (styles['height'] === '1em') height = fontPixels; + else if (/^\d+(\.\d+)?px$/.test(styles['height'] || '')) height = parseFloat(styles['height']); + const width = /^\d+(\.\d+)?px$/.test(styles['width'] || '') ? + parseFloat(styles['width']) : (actual.width * height) / actual.height; + const side = styles['float']; + return new ImageRun({ + data: imageBuffer, + transformation: CodebookDocxGenerator.getTransformation({ width, height }, maxWidth), + ...(side === 'left' || side === 'right' ? { + floating: { + horizontalPosition: { + relative: HorizontalPositionRelativeFrom.COLUMN, + align: side === 'left' ? HorizontalPositionAlign.LEFT : HorizontalPositionAlign.RIGHT + }, + verticalPosition: { relative: VerticalPositionRelativeFrom.PARAGRAPH, offset: 0 }, + layoutInCell: true, + allowOverlap: false, + wrap: { + type: TextWrappingType.SQUARE, + side: side === 'left' ? TextWrappingSide.RIGHT : TextWrappingSide.LEFT, + margins: { [side === 'left' ? 'right' : 'left']: 9525 * 10 } + } + } + } : {}) + }); } - private static getColor( - cheerioAPI: cheerio.CheerioAPI, - tag: cheerio.Cheerio - ): string { - const color = cheerioAPI(tag).css('color'); - // const name = elem.name; - let colorParsed: string = '#000000'; - if (color) { - if (color.startsWith('#')) { - colorParsed = color; - } else if (color.startsWith('rgb')) { - const rgbString = CodebookDocxGenerator.parseCssRgbString(color); - if (rgbString) { - colorParsed = CodebookDocxGenerator.toHex( - rgbString[0], - rgbString[1], - rgbString[2] - ); - } - } else if (WebColors.getHexFromWebColor(color.toLowerCase())) { - colorParsed = `#${WebColors.getHexFromWebColor(color.toLowerCase())}`; - } - } - return colorParsed; + private static inlineStyles(elem: Element): Record { + return Object.fromEntries((elem.attribs['style'] || '').split(';').flatMap(declaration => { + const colon = declaration.indexOf(':'); + if (colon < 0) return []; + const property = declaration.substring(0, colon).trim().toLowerCase(); + const value = declaration.substring(colon + 1).trim(); + return [[property, value]]; + })); } - private static getSize( - cheerioAPI: cheerio.CheerioAPI, - tag: cheerio.Cheerio - ): string { - return cheerioAPI(tag).css('font-size') || '20pt'; + private static cssColor(value: string | undefined, fallback: string): string { + if (!value || value === 'inherit' || value === 'initial' || value === 'currentColor') return fallback; + if (/^#[0-9a-f]{6}$/i.test(value)) return value.substring(1).toUpperCase(); + if (/^#[0-9a-f]{3}$/i.test(value)) { + return value.substring(1).split('').map(part => part + part).join('') + .toUpperCase(); + } + if (value.startsWith('rgb')) { + const rgb = CodebookDocxGenerator.parseCssRgbString(value); + if (rgb?.length === 3 && rgb.every(component => Number.isFinite(component))) { + return CodebookDocxGenerator.toHex(rgb[0], rgb[1], rgb[2]).toUpperCase(); + } + } + return WebColors.getHexFromWebColor(value.toLowerCase())?.replace(/^#/, '').toUpperCase() || fallback; } private static getTextAlignment( @@ -543,14 +590,9 @@ export class CodebookDocxGenerator { return html.replace(/^

    <\/p>/, ''); } - private static convertLineBreaksToHTMLBreaks(html: string): string { - return html.replace(/\n/g, '
    '); - } - private static prepareHtml(html: string): string { - return CodebookDocxGenerator.convertLineBreaksToHTMLBreaks( - CodebookDocxGenerator.stripFromLeadingEmptyParagraph(html) - ); + // Newlines in LaTeX/HTML attributes are data, not paragraph breaks. + return CodebookDocxGenerator.stripFromLeadingEmptyParagraph(html); } private static decodeLatex(latex: string): string { @@ -562,7 +604,7 @@ export class CodebookDocxGenerator { } } - private static latexToOmml(latex: string): ImportedXmlComponent | null { + private static latexToOmml(latex: string, style?: IRunOptions): ImportedXmlComponent | null { const trimmed = latex.trim(); if (!trimmed) return null; try { @@ -572,9 +614,11 @@ export class CodebookDocxGenerator { throwOnError: false, strict: 'ignore' }); - CodebookDocxGenerator.ommlCache[trimmed] = mml2omml(mathml); + CodebookDocxGenerator.ommlCache[trimmed] = CodebookDocxGenerator.normalizeStretchyBars( + mathml, mml2omml(mathml) + ); } - const omml = CodebookDocxGenerator.ommlCache[trimmed]; + const omml = CodebookDocxGenerator.styleMathRuns(CodebookDocxGenerator.ommlCache[trimmed], style); return CodebookDocxGenerator.unwrapImportedXmlRoot( ImportedXmlComponent.fromXmlString( CodebookDocxGenerator.sanitizeOmmlXml(omml) @@ -585,6 +629,100 @@ export class CodebookDocxGenerator { } } + /** The converter treats wide over/underlines as fixed-width accents and loses their position. */ + private static normalizeStretchyBars(mathml: string, omml: string): string { + const math = cheerio.load(mathml, { xml: true }, false); + const positions: Array<'top' | 'bot' | undefined> = []; + math('mover,munder').each((_, node) => { + const operator = math(node).children().last(); + if (operator[0]?.name === 'mo' && operator.text() === '\u203e') { + let position: 'top' | 'bot' | undefined; + if (operator.attr('stretchy') !== 'false') position = node.name === 'munder' ? 'bot' : 'top'; + positions.push(position); + } + }); + if (!positions.some(position => position)) return omml; + const $ = cheerio.load(omml, { xml: true }, false); + let index = 0; + $('m\\:acc').each((_, node) => { + const accent = $(node); + if (accent.children('m\\:accPr').children('m\\:chr').attr('m:val') !== '\u203e') return; + const position = positions[index]; + index += 1; + if (!position) return; + const bar = $(''); + const properties = $('').append(``); + properties.append(accent.children('m\\:accPr').children('m\\:ctrlPr')); + bar.append(properties).append(accent.children('m\\:e')); + accent.replaceWith(bar); + }); + return $.xml(); + } + + /** Apply surrounding editor marks without changing cached LaTeX or unstyled Studio math. */ + private static styleMathRuns(omml: string, style?: IRunOptions): string { + if (!style) return omml; + const properties: Record> = {}; + if (style.bold) properties['b'] = {}; + if (style.italics) properties['i'] = {}; + if (style.underline) properties['u'] = { 'w:val': 'single' }; + if (style.strike) properties['strike'] = {}; + if (style.size !== 20) { + properties['sz'] = { 'w:val': String(style.size) }; + properties['szCs'] = { 'w:val': String(style.size) }; + } + if (style.color && !['#000000', '000000'].includes(style.color)) { + properties['color'] = { 'w:val': style.color.replace(/^#/, '') }; + } + const fill = style.shading?.fill; + if (fill && !['#FFFFFF', 'FFFFFF'].includes(fill)) properties['shd'] = { 'w:fill': fill.replace(/^#/, '') }; + if (!Object.keys(properties).length) return omml; + const $ = cheerio.load(omml, { xml: true }, false); + const applyProperties = (parent: cheerio.Cheerio): void => { + let runProperties = parent.children('w\\:rPr').first(); + if (!runProperties.length) { + parent.append(''); + runProperties = parent.children('w\\:rPr').first(); + } + Object.entries(properties).forEach(([name, attributes]) => { + runProperties.children(`w\\:${name}`).remove(); + runProperties.append(``); + runProperties.children(`w\\:${name}`).last().attr(attributes); + }); + const order = ['b', 'bCs', 'i', 'iCs', 'strike', 'color', 'sz', 'szCs', 'u', 'shd']; + const children = runProperties.children().toArray().sort((a, b) => ( + order.indexOf(a.name.substring(2)) - order.indexOf(b.name.substring(2)) + )); + runProperties.empty().append(children); + }; + $('m\\:r').each((_, run) => { + const element = $(run); + applyProperties(element); + element.prepend(element.children('w\\:rPr').remove()); + const mathProperties = element.children('m\\:rPr').remove(); + if (mathProperties.length) element.prepend(mathProperties); + }); + // Math control properties also style fraction bars, radicals and large operators. + ['acc', 'bar', 'borderBox', 'box', 'd', 'eqArr', 'f', 'func', 'groupChr', + 'limLow', 'limUpp', 'm', 'nary', 'phant', 'rad', 'sPre', 'sSub', 'sSubSup', 'sSup'].forEach(name => { + $(`m\\:${name}`).each((_, node) => { + const element = $(node); + let structureProperties = element.children(`m\\:${name}Pr`).first(); + if (!structureProperties.length) { + element.prepend(``); + structureProperties = element.children(`m\\:${name}Pr`).first(); + } + let control = structureProperties.children('m\\:ctrlPr').first(); + if (!control.length) { + structureProperties.append(''); + control = structureProperties.children('m\\:ctrlPr').first(); + } + applyProperties(control); + }); + }); + return $.xml(); + } + private static unwrapImportedXmlRoot( component: ImportedXmlComponent ): ImportedXmlComponent { @@ -616,12 +754,13 @@ export class CodebookDocxGenerator { } private static createLeftAlignedMathParagraph( - mathChildren: ImportedXmlComponent[] + mathChildren: ImportedXmlComponent[], + alignment = 'left' ): ImportedXmlComponent { const mathParagraph = new ImportedXmlComponent('m:oMathPara'); const mathParagraphProperties = new ImportedXmlComponent('m:oMathParaPr'); mathParagraphProperties.push( - new ImportedXmlComponent('m:jc', { 'm:val': 'left' }) + new ImportedXmlComponent('m:jc', { 'm:val': ['center', 'right'].includes(alignment) ? alignment : 'left' }) ); mathParagraph.push(mathParagraphProperties); mathChildren.forEach(mathChild => mathParagraph.push(mathChild)); @@ -629,7 +768,8 @@ export class CodebookDocxGenerator { } private static normalizeParagraphChildren( - children: ParagraphChild[] + children: ParagraphChild[], + alignment = 'left' ): ParagraphChild[] { if (!children.length) { return children; @@ -639,7 +779,7 @@ export class CodebookDocxGenerator { CodebookDocxGenerator.isMathComponent ) as ImportedXmlComponent[]; if (mathChildren.length === children.length) { - return [CodebookDocxGenerator.createLeftAlignedMathParagraph(mathChildren)]; + return [CodebookDocxGenerator.createLeftAlignedMathParagraph(mathChildren, alignment)]; } return children; @@ -674,10 +814,6 @@ export class CodebookDocxGenerator { ); } - private static isListParagraph(elem: Element): boolean { - return !!elem.parent && (elem.parent as Element).name === 'li'; - } - /** Put loose text and inline marks into paragraphs without duplicating existing blocks. */ private static wrapInlineParagraphs(cheerioAPI: cheerio.CheerioAPI, parent: AnyNode): void { let inlineNodes: AnyNode[] = []; @@ -689,11 +825,11 @@ export class CodebookDocxGenerator { }; cheerioAPI(parent).contents().toArray().forEach(node => { if (node.type === 'tag' && ( - /^(p|h[1-4]|img|ul|ol|li|div|blockquote)$/.test(node.name) || - cheerioAPI(node).find('p,h1,h2,h3,h4,img,ul,ol').length > 0 + /^(p|h[1-4]|img|hr|ul|ol|li|div|blockquote)$/.test(node.name) || + cheerioAPI(node).find('p,h1,h2,h3,h4,img,hr,ul,ol').length > 0 )) { flush(); - if (!/^(p|h[1-4]|img)$/.test(node.name)) { + if (!/^(p|h[1-4]|img|hr)$/.test(node.name)) { CodebookDocxGenerator.wrapInlineParagraphs(cheerioAPI, node); } } else if (node.type === 'text' || node.type === 'tag') { @@ -709,7 +845,7 @@ export class CodebookDocxGenerator { ) { if (!html.trim()) return []; const normalizedHtml = CodebookDocxGenerator.normalizeMathTokens(html); - const blockHtml = /<(p|h[1-4]|img|ul|ol)\b/i.test(normalizedHtml) ? normalizedHtml : `

    ${normalizedHtml}

    `; + const blockHtml = /<(p|h[1-4]|img|hr|ul|ol)\b/i.test(normalizedHtml) ? normalizedHtml : `

    ${normalizedHtml}

    `; const cheerioAPI = cheerio.load( CodebookDocxGenerator.prepareHtml(blockHtml), null, @@ -717,35 +853,122 @@ export class CodebookDocxGenerator { ); CodebookDocxGenerator.wrapInlineParagraphs(cheerioAPI, cheerioAPI.root()[0]); const elements: Paragraph[] = []; - cheerioAPI('p,h1,h2,h3,h4,img').each((i, elem) => { - try { - const span = cheerioAPI(elem).find('span'); - if (elem.name === 'img') { - elements.push(CodebookDocxGenerator.createImagePragraph(elem, contentSetting)); - } else { - elements.push( - CodebookDocxGenerator.createParagraph( + type Context = { + listDepth: number; listNesting: number; listIndent: number; quoteDepth: number; marker?: string; + legacyBullet?: boolean; size: string; color: string; background: string; + }; + const walk = (parent: AnyNode, context: Context): void => { + cheerioAPI(parent).contents().toArray().forEach(node => { + if (node.type !== 'tag') return; + const elem = node as Element; + const styles = CodebookDocxGenerator.inlineStyles(elem); + const size = styles['font-size'] || context.size; + const color = CodebookDocxGenerator.cssColor(styles['color'], context.color); + const background = CodebookDocxGenerator.cssColor(styles['background-color'], context.background); + const inherited = { + ...context, size, color, background + }; + try { + if (elem.name === 'img') { + elements.push(CodebookDocxGenerator.createImagePragraph(elem, contentSetting)); + } else if (/^(p|h[1-4])$/.test(elem.name)) { + const headingSize = { + h1: '20px', h2: '18px', h3: '16px', h4: '16px' + }[elem.name]; + elements.push(CodebookDocxGenerator.createParagraph( cheerioAPI, elem.children, CodebookDocxGenerator.getTextAlignment(cheerioAPI, elem), - CodebookDocxGenerator.getColor(cheerioAPI, span), - CodebookDocxGenerator.getBackgroundColor(cheerioAPI, elem), - CodebookDocxGenerator.getSize(cheerioAPI, span), - CodebookDocxGenerator.isListParagraph(elem) - ) - ); + color, + background, + styles['font-size'] || headingSize || size, + !!context.legacyBullet, + { + element: elem, + marker: context.marker, + listDepth: context.listDepth, + listIndent: context.listIndent, + quoteDepth: context.quoteDepth, + imageWidth: contentSetting.showScore ? 334 : 382 + } + )); + context.marker = undefined; + context.legacyBullet = false; + } else if (elem.name === 'ul' || elem.name === 'ol') { + const style = styles['list-style-type'] || styles['list-style'] || (elem.name === 'ol' ? 'decimal' : 'disc'); + const legacy = elem.name === 'ul' && !styles['list-style-type'] && + !styles['list-style'] && context.listNesting === 0; + const parsedStart = Number.parseInt(elem.attribs['start'], 10); + const start = Number.isFinite(parsedStart) ? parsedStart : 1; + cheerioAPI(elem).children('li').toArray().forEach((li, index) => { + const padding = parseFloat(CodebookDocxGenerator.inlineStyles(li)['padding-left']) || 0; + walk(li, { + ...inherited, + marker: legacy ? undefined : CodebookDocxGenerator.listMarker(style, start + index), + legacyBullet: legacy, + listDepth: legacy ? 0 : context.listNesting + 1, + listNesting: context.listNesting + 1, + listIndent: context.listIndent + Math.round(padding * 15) + }); + }); + } else if (elem.name === 'hr') { + const short = styles['width'] === '20%' || elem.attribs['short'] === 'true'; + elements.push(new Paragraph({ + border: { bottom: { color: '000000', style: 'single', size: 6 } }, + indent: short ? { end: Math.round((contentSetting.showScore ? 334 : 382) * 15 * 0.8) } : undefined, + spacing: { before: 100, after: 100 } + })); + } else { + walk(elem, { ...inherited, quoteDepth: context.quoteDepth + Number(elem.name === 'blockquote') }); + } + } catch { + elements.push(new Paragraph({ text: 'HTML konnte nicht verarbeitet werden.' })); } - } catch { - elements.push( - new Paragraph({ - text: 'HTML konnte nicht verarbeitet werden.' - }) - ); - } + }); + }; + walk(cheerioAPI.root()[0], { + listDepth: 0, + listNesting: 0, + listIndent: 0, + quoteDepth: 0, + size: '10pt', + color: '#000000', + background: '#FFFFFF' }); return elements.filter(e => e !== undefined && e !== null); } + /** Literal markers retain all CSS list styles, including Greek, in a static Word export. */ + private static listMarker(style: string, value: number): string { + const bullets: Record = { disc: '•', circle: '○', square: '▪' }; + if (bullets[style]) return bullets[style]; + if (style.includes('roman')) { + let remainder = Math.min(10000, Math.max(1, Math.floor(value))); + let roman = ''; + const numbers: [number, string][] = [[1000, 'M'], [900, 'CM'], [500, 'D'], [400, 'CD'], + [100, 'C'], [90, 'XC'], [50, 'L'], [40, 'XL'], [10, 'X'], [9, 'IX'], [5, 'V'], [4, 'IV'], [1, 'I']]; + numbers.forEach(([number, symbol]) => { + while (remainder >= number) { + roman += symbol; + remainder -= number; + } + }); + return `${style.startsWith('lower') ? roman.toLowerCase() : roman}.`; + } + if (['lower-latin', 'upper-latin', 'lower-alpha', 'upper-alpha', 'lower-greek'].includes(style)) { + const alphabet = style === 'lower-greek' ? 'αβγδεζηθικλμνξοπρστυφχψω' : 'abcdefghijklmnopqrstuvwxyz'; + let number = Math.max(1, Math.floor(value)); + let marker = ''; + while (number > 0) { + number -= 1; + marker = alphabet[number % alphabet.length] + marker; + number = Math.floor(number / alphabet.length); + } + return `${style.startsWith('upper') ? marker.toUpperCase() : marker}.`; + } + return `${value}.`; + } + private static getFontSize(size: string): number { const sizeTypes = [ 'xx-small', @@ -756,7 +979,11 @@ export class CodebookDocxGenerator { 'x-large', 'xx-large' ]; - return sizeTypes.includes(size) ? 20 : parseInt(size, 10); + if (sizeTypes.includes(size)) return 20; + const number = parseFloat(size); + if (!Number.isFinite(number) || number <= 0) return 20; + // CSS pixels are 1/96 inch; Word stores font sizes in half-points (1/144 inch). + return Math.max(1, Math.min(1638, Math.round(number * (size.endsWith('px') ? 1.5 : 2)))); } private static getAlignment( @@ -781,28 +1008,52 @@ export class CodebookDocxGenerator { colorParsed: string, backgroundColor: string, size: string, - isListParagraph: boolean + isListParagraph: boolean, + options?: InstructionParagraph ): Paragraph { const children: ParagraphChild[] = []; + if (options?.marker) { + children.push(new TextRun({ + children: [options.marker, new Tab()], size: CodebookDocxGenerator.getFontSize(size) + })); + } const childNodes = CodebookDocxGenerator.getChildren(cheerioAPI, elem); CodebookDocxGenerator.processInlineElements( childNodes, children, colorParsed, backgroundColor, - size + size, + options?.element.name.match(/^h[1-3]$/) ? { bold: true } : {}, + options?.imageWidth ); const normalizedChildren = CodebookDocxGenerator.normalizeParagraphChildren( - children + children, + textAlignment ); + const styles = options ? CodebookDocxGenerator.inlineStyles(options.element) : {}; + const pixels = (value: string | undefined) => { + const amount = parseFloat(value || ''); + return Number.isFinite(amount) ? Math.round(amount * (value?.endsWith('pt') ? 20 : 15)) : undefined; + }; + const listStart = (options?.listDepth || 0) * 600 + (options?.listIndent || 0); + const start = (pixels(styles['padding-left']) ?? (isListParagraph ? 0 : 100)) + listStart + (options?.quoteDepth || 0) * 600; + const textIndent = pixels(styles['text-indent']) || 0; + const indent = options?.marker ? { start, hanging: 300 } : { + start, + end: 100, + ...(textIndent < 0 ? { hanging: -textIndent } : {}), + ...(textIndent > 0 ? { firstLine: textIndent } : {}) + }; return new Paragraph({ alignment: CodebookDocxGenerator.getAlignment(textAlignment), spacing: { - before: 100, - after: 100 + before: pixels(styles['margin-top']) ?? 100, + after: pixels(styles['margin-bottom']) ?? 100 }, - indent: !isListParagraph ? { start: 100, end: 100 } : undefined, + indent: isListParagraph ? undefined : indent, bullet: isListParagraph ? { level: 0 } : undefined, + ...(options?.marker ? { tabStops: [{ type: TabStopType.LEFT, position: start }] } : {}), children: normalizedChildren }); } diff --git a/projects/ngx-coding-components/codebook-generator/manual-instruction.ts b/projects/ngx-coding-components/codebook-generator/manual-instruction.ts index 460633a..7f7d38d 100644 --- a/projects/ngx-coding-components/codebook-generator/manual-instruction.ts +++ b/projects/ngx-coding-components/codebook-generator/manual-instruction.ts @@ -8,5 +8,6 @@ export function hasCodebookManualInstruction(instruction?: string | null): boole const text = $.root().text().replace(/[\s\p{Default_Ignorable_Code_Point}]/gu, ''); if (text) return true; return $('img').toArray().some(element => /^data:image\/(png|jpeg|jpg|gif|bmp);base64,\S+$/i.test($(element).attr('src') || '')) || - $('span.iqb-math-formula').toArray().some(element => !!$(element).attr('data-latex')?.trim()); + $('span.iqb-math-formula').toArray().some(element => !!$(element).attr('data-latex')?.trim()) || + $('aspect-nodeview-math-formula').toArray().some(element => !!$(element).attr('formula')?.trim()); } diff --git a/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/hanging-indent.ts b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/hanging-indent.ts index 27de450..f239988 100644 --- a/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/hanging-indent.ts +++ b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/hanging-indent.ts @@ -1,5 +1,5 @@ import { Command, Extension } from '@tiptap/core'; -import { TextSelection, Transaction } from 'prosemirror-state'; +import { AllSelection, TextSelection, Transaction } from 'prosemirror-state'; declare module '@tiptap/core' { interface Commands { @@ -30,11 +30,11 @@ export const HangingIndent = Extension.create({ { style: `text-indent: -${attributes['indentSize']}px` } : { style: 'text-indent: 0px' } ), - parseHTML: element => element.style.textIndent !== '0px' + parseHTML: element => parseFloat(element.style.textIndent) < 0 }, indentSize: { default: 20, - parseHTML: element => Number(element.getAttribute('indentSize')) + parseHTML: element => Number(element.getAttribute('indentSize')) || 20 } } } @@ -57,11 +57,14 @@ export const HangingIndent = Extension.create({ const updateIndentLevel = (tr: Transaction, hangingIndent: boolean, indentSize: number): Transaction => { const { doc, selection } = tr; - if (doc && selection && (selection instanceof TextSelection)) { + if (doc && selection && (selection instanceof TextSelection || selection instanceof AllSelection)) { const { from, to } = selection; doc.nodesBetween(from, to, (node, pos) => { - setNodeIndentMarkup(tr, pos, hangingIndent, indentSize); - return false; + if (this.options.types.includes(node.type.name)) { + setNodeIndentMarkup(tr, pos, hangingIndent, indentSize); + return false; + } + return true; }); } diff --git a/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/indent-selection.spec.ts b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/indent-selection.spec.ts new file mode 100644 index 0000000..e7397c5 --- /dev/null +++ b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/indent-selection.spec.ts @@ -0,0 +1,80 @@ +import { Editor } from '@tiptap/core'; +import { Document } from '@tiptap/extension-document'; +import { Text } from '@tiptap/extension-text'; +import { BulletList } from '@tiptap/extension-bullet-list'; +import { ListItem } from '@tiptap/extension-list-item'; +import { Indent } from './indent'; +import { HangingIndent } from './hanging-indent'; +import { ParagraphExtension } from './paragraph-extension'; + +describe('Indent selection', () => { + let editor: Editor; + beforeEach(() => { + editor = new Editor({ + extensions: [Document, ParagraphExtension, Text, BulletList, ListItem, + Indent.configure({ types: ['paragraph', 'listItem'], maxLevel: 4 }), HangingIndent] + }); + }); + afterEach(() => editor.destroy()); + + it('indents every paragraph selected by Select All and retains the result on reload', () => { + editor.commands.setContent('

    A

    B

    '); + editor.commands.selectAll(); + expect(editor.commands.indent(20)).toBe(true); + expect(editor.getJSON().content?.map(node => node.attrs?.['indent'])).toEqual([1, 1]); + const html = editor.getHTML(); + editor.commands.setContent(html); + expect(editor.getHTML()).toBe(html); + editor.commands.selectAll(); + editor.commands.outdent(20); + expect(editor.getJSON().content?.map(node => node.attrs?.['indent'])).toEqual([0, 0]); + }); + + it('applies and removes hanging indent through list containers without altering their nodes', () => { + editor.commands.setContent('
    • A

    • B

    '); + editor.commands.selectAll(); + editor.commands.hangIndent(20); + const paragraphs: boolean[] = []; + editor.state.doc.descendants(node => { + if (node.type.name === 'paragraph') paragraphs.push(node.attrs['hangingIndent']); + if (node.type.name === 'bulletList') expect(node.attrs['hangingIndent']).toBeUndefined(); + }); + expect(paragraphs).toEqual([true, true]); + editor.commands.unhangIndent(20); + expect(editor.getHTML()).not.toContain('text-indent: -'); + }); + + it('keeps the default indent size and clamps repeated commands for a text selection', () => { + editor.commands.setContent('

    A

    '); + expect(editor.getAttributes('paragraph')['indentSize']).toBe(20); + expect(editor.getAttributes('paragraph')['hangingIndent']).toBe(false); + editor.commands.setTextSelection(1); + for (let i = 0; i < 6; i++) editor.commands.indent(20); + expect(editor.getAttributes('paragraph')['indent']).toBe(4); + for (let i = 0; i < 6; i++) editor.commands.outdent(20); + expect(editor.getAttributes('paragraph')['indent']).toBe(0); + }); + + it('sets spacing on filled paragraphs for both a caret and Select All', () => { + editor.commands.setContent('

    A

    B

    '); + editor.commands.setTextSelection(1); + expect(editor.commands.setMargin(10)).toBe(true); + expect(editor.getJSON().content?.map(node => node.attrs?.['margin'])).toEqual([10, 0]); + editor.commands.selectAll(); + expect(editor.commands.setMargin(20)).toBe(true); + expect(editor.getJSON().content?.map(node => node.attrs?.['margin'])).toEqual([20, 20]); + expect(editor.getText()).toBe('A\n\nB'); + }); + + it('sets spacing inside list items and on an empty paragraph', () => { + editor.commands.setContent('
    • A

    '); + editor.commands.selectAll(); + editor.commands.setMargin(10); + const margins: number[] = []; + editor.state.doc.descendants(node => { + if (node.type.name === 'paragraph') margins.push(node.attrs['margin']); + else expect(node.attrs['margin']).toBeUndefined(); + }); + expect(margins).toEqual([10, 10]); + }); +}); diff --git a/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/indent.ts b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/indent.ts index 6faf3fd..150b380 100644 --- a/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/indent.ts +++ b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/indent.ts @@ -1,5 +1,5 @@ import { Command, Extension } from '@tiptap/core'; -import { TextSelection, Transaction } from 'prosemirror-state'; +import { AllSelection, TextSelection, Transaction } from 'prosemirror-state'; export interface IndentOptions { types: string[]; @@ -44,7 +44,7 @@ export const Indent = Extension.create({ }, indentSize: { default: 20, - parseHTML: element => Number(element.getAttribute('indentSize')) + parseHTML: element => Number(element.getAttribute('indentSize')) || 20 } } } @@ -77,7 +77,7 @@ export const Indent = Extension.create({ const { doc, selection } = tr; let transaction = tr; - if (doc && selection && (selection instanceof TextSelection)) { + if (doc && selection && (selection instanceof TextSelection || selection instanceof AllSelection)) { const { from, to } = selection; doc.nodesBetween(from, to, (node, pos) => { if (this.options.types.includes(node.type.name)) { diff --git a/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/iqb-math-formula.spec.ts b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/iqb-math-formula.spec.ts new file mode 100644 index 0000000..8bf7556 --- /dev/null +++ b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/iqb-math-formula.spec.ts @@ -0,0 +1,31 @@ +import { Editor } from '@tiptap/core'; +import { Document } from '@tiptap/extension-document'; +import { Paragraph } from '@tiptap/extension-paragraph'; +import { Text } from '@tiptap/extension-text'; +import { Bold } from '@tiptap/extension-bold'; +import IqbMathFormula from './iqb-math-formula'; + +describe('IqbMathFormula input interoperability', () => { + let editor: Editor; + beforeEach(() => { + editor = new Editor({ extensions: [Document, Paragraph, Text, Bold, IqbMathFormula] }); + }); + afterEach(() => editor.destroy()); + + [ + 'STALE', + 'STALE' + ].forEach(tag => { + it(`imports native LaTeX from ${tag.startsWith(' { + editor.commands.setContent(`

    Vor ${tag} Nach

    `); + const formula = editor.getJSON().content?.[0].content?.[1]; + expect(formula?.type).toBe('iqbMathFormula'); + expect(formula?.attrs?.['latex']).toBe('\\frac{1}{2}'); + expect(formula?.marks).toEqual([{ type: 'bold' }]); + expect(editor.getHTML()).not.toContain('STALE'); + const html = editor.getHTML(); + editor.commands.setContent(html); + expect(editor.getHTML()).toBe(html); + }); + }); +}); diff --git a/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/iqb-math-formula.ts b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/iqb-math-formula.ts index daa1ab9..af87251 100644 --- a/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/iqb-math-formula.ts +++ b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/iqb-math-formula.ts @@ -34,13 +34,13 @@ const IqbMathFormula = Node.create({ return { latex: { default: '', - parseHTML: element => element.getAttribute('data-latex') || '' + parseHTML: element => element.getAttribute('data-latex') || element.getAttribute('formula') || '' } }; }, parseHTML() { - return [{ tag: 'span.iqb-math-formula[data-latex]' }]; + return [{ tag: 'span.iqb-math-formula[data-latex]' }, { tag: 'aspect-nodeview-math-formula[formula]' }]; }, renderHTML({ HTMLAttributes }) { diff --git a/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/paragraph-extension.ts b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/paragraph-extension.ts index 079d8ae..d2f50a1 100644 --- a/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/paragraph-extension.ts +++ b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/paragraph-extension.ts @@ -1,6 +1,5 @@ import { Paragraph } from '@tiptap/extension-paragraph'; import { Command } from '@tiptap/core'; -import { Transaction } from 'prosemirror-state'; declare module '@tiptap/core' { interface Commands { @@ -24,23 +23,17 @@ export const ParagraphExtension = Paragraph.extend({ }, addCommands() { - const setNodeIndentMarkup = (tr: Transaction, pos: number, newMargin: number): Transaction => { - const node = tr?.doc?.nodeAt(pos); - if (node) { - const nodeAttrs = { ...node.attrs, margin: newMargin }; - return tr.setNodeMarkup(pos, node.type, nodeAttrs, node.marks); - } - return tr; - }; + const { name } = this; const applyMargin: (newMargin: number) => () => Command = newMargin => () => ({ tr, state, dispatch }) => { const { selection } = state; - let transaction; tr.doc.nodesBetween(selection.from, selection.to, (node, pos) => { - transaction = setNodeIndentMarkup(tr, pos, newMargin); + // Text nodes cannot receive node markup; only this paragraph owns margin. + if (node.type.name !== name) return; + tr.setNodeMarkup(pos, node.type, { ...node.attrs, margin: newMargin }, node.marks); }); - dispatch?.(transaction); + dispatch?.(tr); return true; }; diff --git a/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/preserve-whitespace.spec.ts b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/preserve-whitespace.spec.ts new file mode 100644 index 0000000..024f7c8 --- /dev/null +++ b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/preserve-whitespace.spec.ts @@ -0,0 +1,45 @@ +import { Editor } from '@tiptap/core'; +import { Document } from '@tiptap/extension-document'; +import { Paragraph } from '@tiptap/extension-paragraph'; +import { Text } from '@tiptap/extension-text'; +import { Bold } from '@tiptap/extension-bold'; +import { PreserveWhitespace } from './preserve-whitespace'; + +describe('PreserveWhitespace', () => { + let editor: Editor; + beforeEach(() => { + editor = new Editor({ extensions: [Document, Paragraph, Text, Bold, PreserveWhitespace] }); + }); + afterEach(() => editor.destroy()); + + it('retains repeated and non-breaking spaces through the model write command and reload', () => { + editor.commands.setContent('

    A B C D E F

    '); + expect(editor.getText()).toBe('A B\u00a0C\u202fD E F'); + const stored = editor.getHTML(); + editor.commands.setContent(stored); + expect(editor.getHTML()).toBe(stored); + }); + + it('honors an explicitly requested parser override and the emitUpdate flag', () => { + let updates = 0; + editor.on('update', () => { updates += 1; }); + editor.commands.setContent('

    A B

    ', false, { preserveWhitespace: false }); + expect(editor.getText()).toBe('A B'); + expect(updates).toBe(0); + editor.commands.setContent('

    A B

    ', true); + expect(editor.getText()).toBe('A B'); + expect(updates).toBe(1); + }); + + it('keeps JSON content and marks unchanged', () => { + const content = { + type: 'doc', + content: [{ + type: 'paragraph', + content: [{ type: 'text', text: 'A B\u00a0C', marks: [{ type: 'bold' }] }] + }] + }; + editor.commands.setContent(content); + expect(editor.getJSON()).toEqual(content); + }); +}); diff --git a/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/preserve-whitespace.ts b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/preserve-whitespace.ts new file mode 100644 index 0000000..ab004ae --- /dev/null +++ b/projects/ngx-coding-components/src/lib/rich-text-editor/extensions/preserve-whitespace.ts @@ -0,0 +1,15 @@ +import { extensions, RawCommands } from '@tiptap/core'; + +/** Preserve authored spacing when ngx-tiptap writes stored HTML through setContent. */ +export const PreserveWhitespace = extensions.Commands.extend({ + name: 'preserveWhitespace', + priority: 1, + addCommands() { + const originalSetContent = this.parent?.()?.setContent; + if (!originalSetContent) throw new Error('The Tiptap setContent command is required.'); + const setContent: RawCommands['setContent'] = (content, emitUpdate = false, parseOptions = {}, options = {}) => ( + originalSetContent(content, emitUpdate, { preserveWhitespace: 'full', ...parseOptions }, options) + ); + return { setContent }; + } +}); diff --git a/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.html b/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.html index 5191787..ec58096 100644 --- a/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.html +++ b/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.html @@ -79,7 +79,7 @@ Abstand - 0px diff --git a/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.ts b/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.ts index 4b7b8d9..f270a81 100644 --- a/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.ts +++ b/projects/ngx-coding-components/src/lib/rich-text-editor/rich-text-editor.component.ts @@ -30,6 +30,7 @@ import { MatIcon } from '@angular/material/icon'; import { MatTooltip } from '@angular/material/tooltip'; import { MatIconButton, MatButton } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; +import { PreserveWhitespace } from './extensions/preserve-whitespace'; import { AnchorId } from './extensions/anchorId'; import { Indent } from './extensions/indent'; import { HangingIndent } from './extensions/hanging-indent'; @@ -75,7 +76,7 @@ export class RichTextEditorComponent implements AfterViewInit, OnChanges { orderedListStyle: string = 'decimal'; defaultExtensions = [ - Document, Text, ListItem, + Document, Text, ListItem, PreserveWhitespace, Underline, Superscript, Subscript, TextStyle, Color, Bold, Italic, Strike, History, @@ -107,6 +108,7 @@ export class RichTextEditorComponent implements AfterViewInit, OnChanges { ]; editor: Editor = new Editor({ + parseOptions: { preserveWhitespace: 'full' }, extensions: this.defaultExtensions }); diff --git a/scripts/codebook-contract.test.mjs b/scripts/codebook-contract.test.mjs index 876821d..b437f51 100644 --- a/scripts/codebook-contract.test.mjs +++ b/scripts/codebook-contract.test.mjs @@ -126,9 +126,9 @@ function descriptionParagraphs(xml) { for (const tag of ['ul', 'ol']) { test(`DOCX retains direct ${tag} list text and existing paragraph list items exactly once`, async () => { const { xml } = await document(`<${tag}>
  • Erstes Kriterium
  • Zweites Kriterium

  • `); - assert.deepEqual(descriptionParagraphs(xml), ['Erstes Kriterium', 'Zweites Kriterium']); + assert.deepEqual(descriptionParagraphs(xml), tag === 'ol' ? ['1.Erstes Kriterium', '2.Zweites Kriterium'] : ['Erstes Kriterium', 'Zweites Kriterium']); const $ = load(xml, { xml: true }); - assert.equal($('w\\:numPr').length, 2); + assert.equal($('w\\:numPr').length, tag === 'ul' ? 2 : 0); assert.equal($('w\\:r').filter((_, run) => $(run).find('w\\:t').text() === 'Kriterium').find('w\\:b').length, 1); }); } @@ -144,7 +144,7 @@ test('DOCX keeps loose text and formatting before, between and after blocks in s test('DOCX keeps mixed and nested list contents without repeating paragraphs or images', async () => { const image = 'iVBORw0KGgoAAAANSUhEUgAAACgAAAAUCAIAAABwJOjsAAAAJElEQVR4nGOQSDgwIIhh1OJRi0ctHrV41OJRi0ctHrV45FgMAHBlzy43YLJ+AAAAAElFTkSuQmCC'; const { xml, zip } = await document(`
    • Vor

      Absatz

      Danach
      • Unterpunkt [[iqb-math:x^2]]
    `); - assert.deepEqual(descriptionParagraphs(xml), ['Vor', 'Absatz', 'Danach', 'Unterpunkt ', '']); + assert.deepEqual(descriptionParagraphs(xml), ['Vor', 'Absatz', 'Danach', '•Unterpunkt ', '']); const $ = load(xml, { xml: true }); assert.equal($('w\\:drawing').length, 1); assert.equal($('m\\:oMath').length, 1); diff --git a/scripts/codebook-docx-test-utils.mjs b/scripts/codebook-docx-test-utils.mjs index 223b31c..c283e0f 100644 --- a/scripts/codebook-docx-test-utils.mjs +++ b/scripts/codebook-docx-test-utils.mjs @@ -1,3 +1,4 @@ +import assert from 'node:assert/strict'; import { createHash } from 'node:crypto'; import JSZip from 'jszip'; @@ -20,10 +21,14 @@ export async function docxParts(output) { if (file.dir || name === 'docProps/core.xml') continue; // creation/modification timestamps, not document layout const stableName = name.startsWith('word/media/') ? `word/media/${mediaNames.get(name.slice(11)) || name.slice(11)}` : name; result[stableName] = name.endsWith('.xml') || name.endsWith('.rels') ? - (await file.async('string')).replaceAll(new Date().toLocaleDateString(), '{{DATE}}') - .replaceAll('IQB-Studio Codebook', 'IQB Codebook') : + await file.async('string') : await file.async('nodebuffer'); if (typeof result[stableName] === 'string') { + if (/^word\/footer\d+\.xml$/.test(name)) { + result[stableName] = result[stableName].replaceAll('IQB-Studio Codebook', 'IQB Codebook') + .replace(/(]*>IQB Codebook <\/w:t>[\s\S]*?]*>)[\d./-]+(<\/w:t>)/g, '$1{{DATE}}$2') + .replace(/(IQB Codebook )[\d./-]+(?=<\/w:t>)/g, '$1{{DATE}}'); + } for (const [random, stable] of mediaNames) result[stableName] = result[stableName].replaceAll(random, stable); } } @@ -32,3 +37,19 @@ export async function docxParts(output) { export function partHashes(parts) { return Object.fromEntries(Object.entries(parts).map(([name, content]) => [name, createHash('sha256').update(content).digest('hex')])); } + +// Undo only the independently specified correction in the old synthetic Farb-para +// before hashing against the immutable Studio oracle. All other parts stay exact. +export function originalStudioCompatibleParts(parts) { + const result = { ...parts }; + result['word/document.xml'] = result['word/document.xml'].replace(/]*)?>[\s\S]*?<\/w:p>/g, paragraph => { + if (!paragraph.includes('>Farbe')) return paragraph; + assert.match(paragraph, //, 'authored 18pt must become 36 Word half-points'); + assert.match(paragraph, //); + assert.match(paragraph, /w:fill="FFFF00"/); + return paragraph.replace('', '') + .replace('', '') + .replace('w:fill="FFFF00"', 'w:fill="ffff00"'); + }); + return result; +} diff --git a/scripts/codebook-docx-test-utils.test.mjs b/scripts/codebook-docx-test-utils.test.mjs new file mode 100644 index 0000000..94110db --- /dev/null +++ b/scripts/codebook-docx-test-utils.test.mjs @@ -0,0 +1,20 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import JSZip from 'jszip'; +import { docxParts } from './codebook-docx-test-utils.mjs'; + +test('document comparison normalizes the footer date across days without changing user content', async () => { + const document = 'IQB-Studio Codebook 1/2/2020'; + for (const date of ['1/2/2020', '13.10.2026', '2026-10-14']) { + for (const text of [`IQB-Studio Codebook ${date}`, + `IQB-Studio Codebook ${date}`]) { + const zip = new JSZip(); + zip.file('word/document.xml', document); + zip.file('word/footer1.xml', `${text}`); + const parts = await docxParts(await zip.generateAsync({ type: 'nodebuffer' })); + assert.equal(parts['word/document.xml'], document); + assert.ok(parts['word/footer1.xml'].includes('{{DATE}}')); + assert.ok(!parts['word/footer1.xml'].includes(date)); + } + } +}); diff --git a/scripts/codebook-matrix.test.mjs b/scripts/codebook-matrix.test.mjs index ade5dd3..bd104dc 100644 --- a/scripts/codebook-matrix.test.mjs +++ b/scripts/codebook-matrix.test.mjs @@ -4,7 +4,7 @@ import { readFile } from 'node:fs/promises'; import { load } from 'cheerio'; import { CodebookGenerator } from '../dist/ngx-coding-components/fesm2022/iqb-ngx-coding-components-codebook-generator.mjs'; import { defaults, selectionCorpus, expectedSelection, optionMatrix, missingCases, referenceCorpus, referenceOptions } from './fixtures/codebook-synthetic.mjs'; -import { docxParts, partHashes } from './codebook-docx-test-utils.mjs'; +import { docxParts, partHashes, originalStudioCompatibleParts } from './codebook-docx-test-utils.mjs'; const corpus = selectionCorpus(); const compactRows = corpus.rows.filter(({ variable }) => variable.sourceType === 'BASE' || @@ -84,7 +84,7 @@ test('raw synthetic schemas match original Studio JSON and complete DOCX parts f const json = JSON.parse(await (await CodebookGenerator.generateCodebook(units, { ...options, exportFormat: 'json' }, missings)).text()); assert.deepEqual(json, entry.json, `original Studio raw-schema JSON: case ${index}`); const parts = await docxParts(await CodebookGenerator.generateCodebook(units, options, missings)); - assert.deepEqual(partHashes(parts), entry.docxParts, `original Studio DOCX: case ${index}`); + assert.deepEqual(partHashes(originalStudioCompatibleParts(parts)), entry.docxParts, `original Studio DOCX: case ${index}`); } assert.equal(index, 32); }); diff --git a/scripts/codebook-rich-layout.test.mjs b/scripts/codebook-rich-layout.test.mjs new file mode 100644 index 0000000..1712b8f --- /dev/null +++ b/scripts/codebook-rich-layout.test.mjs @@ -0,0 +1,203 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { load } from 'cheerio'; +import { + CodebookDocxGenerator, + CodebookGenerator, + hasCodebookManualInstruction, +} from '../dist/ngx-coding-components/fesm2022/iqb-ngx-coding-components-codebook-generator.mjs'; +import { defaults, image } from './fixtures/codebook-synthetic.mjs'; +import { docxParts } from './codebook-docx-test-utils.mjs'; +async function render(html) { + const parts = await docxParts( + await CodebookDocxGenerator.generateDocx( + [ + { + key: 'U', + name: 'Formatierung', + items: [], + missings: [], + variables: [ + { + id: 'V', + label: 'Instruktion', + sourceType: 'BASE', + generalInstruction: '', + codes: [{ id: '1', label: 'Code', score: '1', description: html }], + }, + ], + }, + ], + defaults, + ), + ); + return load(parts['word/document.xml'], { xml: true }); +} +const runs = ($, text) => + $('w\\:r') + .toArray() + .filter((r) => $(r).children('w\\:t').text() === text); +for (const start of [0, 7]) { + test(`ordered decimal lists retain their explicit start ${start}`, async () => { + const $ = await render(`
    1. ONE

    2. TWO

    `); + assert.ok($(runs($, 'ONE')[0]).closest('w\\:p').find('w\\:t').text().startsWith(`${start}.`)); + assert.ok($(runs($, 'TWO')[0]).closest('w\\:p').find('w\\:t').text().startsWith(`${start + 1}.`)); + }); +} +for (const [command, position] of [['overline', 'top'], ['underline', 'bot']]) { + test(`wide ${command} covers the full expression at the correct position`, async () => { + const $ = await render(`

    [[iqb-math:\\${command}{S\\cap M}]]

    `); + assert.equal($('m\\:barPr m\\:pos').attr('m:val'), position); + assert.equal($('m\\:bar > m\\:e').text(), 'S∩M'); + assert.equal($('m\\:acc').length, 0); + }); +} +test('nested over/underlines keep both positions and a narrow bar accent remains an accent', async () => { + const $ = await render('

    [[iqb-math:\\overline{\\underline{S\\cap M}}]]

    [[iqb-math:\\bar{x}]]

    '); + assert.deepEqual($('m\\:barPr m\\:pos').toArray().map(node => $(node).attr('m:val')), ['top', 'bot']); + assert.equal($('m\\:accPr m\\:chr').attr('m:val'), 'ˉ'); +}); +for (const [latex, structure] of [['\\frac{1}{2}', 'f'], ['\\sqrt{x}', 'rad'], + ['\\sum_{i=1}^{3}i', 'nary'], ['\\begin{pmatrix}1&2\\\\3&4\\end{pmatrix}', 'm']]) { + test(`compound math ${structure} inherits size and color for its control symbols`, async () => { + const html = `

    ` + + `` + + '

    '; + const $ = await render(html); + const control = $(`m\\:${structure}Pr > m\\:ctrlPr > w\\:rPr`); + assert.equal(control.find('w\\:sz').attr('w:val'), '54'); + assert.equal(control.find('w\\:color').attr('w:val'), 'FF0000'); + assert.equal(control.find('w\\:b').length, 1); + }); +} +test('surrounding font size, color and marks also apply to native Aspect formulas', async () => { + const $ = await render('

    Vor ' + + '' + + ' Nach

    [[iqb-math:x^2]]

    '); + const math = $('m\\:oMath'); + math.first().find('m\\:r').each((_, run) => { + assert.equal($(run).find('w\\:sz').attr('w:val'), '54'); + assert.equal($(run).find('w\\:color').attr('w:val'), 'FF0000'); + assert.equal($(run).find('w\\:b').length, 1); + assert.equal($(run).find('w\\:u').attr('w:val'), 'single'); + }); + assert.equal(math.last().find('w\\:sz,w\\:color,w\\:b,w\\:u').length, 0, 'unstyled math must not inherit cached marks'); +}); + +test('Aspect tooltip text and visible anchor highlight survive static export', async () => { + const $ = await render('

    SICHTBAR' + + 'BEREICH

    '); + assert.equal($(runs($, 'SICHTBAR')[0]).find('w\\:t').text(), 'SICHTBAR'); + assert.equal($(runs($, 'BEREICH')[0]).find('w\\:shd').attr('w:fill'), 'DCCCE6'); + assert.equal($(runs($, 'BEREICH')[0]).find('w\\:i').length, 1); +}); +test('inline color, size and highlight stay inside their authored text boundaries', async () => { + const $ = await render( + '

    Vor ROT Schwarz GRUEN Ende

    ', + ); + const r = (t) => $(runs($, t)[0]); + assert.equal(r('Vor ').find('w\\:sz').attr('w:val'), '20'); + assert.equal(r('Vor ').find('w\\:color').attr('w:val'), '000000'); + assert.equal(r('ROT').find('w\\:sz').attr('w:val'), '36'); + assert.equal(r('ROT').find('w\\:color').attr('w:val'), 'FF0000'); + assert.equal(r('GRUEN').find('w\\:shd').attr('w:fill'), '00FF00'); + assert.equal(r(' Ende').find('w\\:shd').attr('w:fill'), 'FFFFFF'); +}); +for (const [size, halfPoints] of [ + ['8px', 12], + ['18px', 27], + ['24px', 36], + ['36px', 54], + ['18pt', 36], +]) + test(`font size ${size} uses Word half-points`, async () => { + const $ = await render(`

    SIZE

    `); + assert.equal($(runs($, 'SIZE')[0]).find('w\\:sz').attr('w:val'), String(halfPoints)); + }); +test('paragraph spacing, indent and hanging indent survive editor serialization', async () => { + const $ = await render('

    INDENT

    '); + const p = $(runs($, 'INDENT')[0]).closest('w\\:p'); + assert.equal(p.find('w\\:ind').attr('w:start'), '600'); + assert.equal(p.find('w\\:ind').attr('w:hanging'), '300'); + assert.equal(p.find('w\\:spacing').attr('w:before'), '0'); + assert.equal(p.find('w\\:spacing').attr('w:after'), '150'); +}); +for (const [style, marker] of [ + ['decimal', '1.'], + ['lower-latin', 'a.'], + ['upper-latin', 'A.'], + ['lower-roman', 'i.'], + ['upper-roman', 'I.'], + ['lower-greek', 'α.'], + ['disc', '•'], + ['circle', '○'], + ['square', '▪'], +]) + test(`authored list style ${style} is retained`, async () => { + const tag = ['disc', 'circle', 'square'].includes(style) ? 'ul' : 'ol'; + const $ = await render(`<${tag} style="list-style:${style}">
  • ONE

  • TWO

  • `); + const p = $(runs($, 'ONE')[0]).closest('w\\:p'); + assert.ok(p.text().startsWith(marker), p.text()); + }); +test('inline image stays between text runs, without duplicate paragraph image', async () => { + const $ = await render( + `

    vornach

    `, + ); + const p = $(runs($, 'vor')[0]).closest('w\\:p'); + assert.equal(p.find('w\\:drawing').length, 1); + assert.equal($('w\\:drawing').length, 1); + assert.equal(p.find('w\\:t').text(), 'vornach'); +}); +for (const side of ['left', 'right']) + test(`block image float ${side} is retained in Word`, async () => { + const $ = await render( + `

    Daneben

    `, + ); + assert.equal($('wp\\:anchor').length, 1); + assert.equal($('wp\\:positionH wp\\:align').text(), side); + assert.equal($('wp\\:wrapSquare').length, 1); + }); +test('Aspect formula renders from LaTeX once instead of frozen MathLive fragments', async () => { + const $ = await render( + '

    vorSTALEnach

    ', + ); + assert.equal($('m\\:oMath').length, 1); + assert.ok(!$('w\\:t').text().includes('STALE')); +}); +test('math-only paragraph respects authored center alignment', async () => { + const $ = await render('

    [[iqb-math:x^2]]

    '); + assert.equal($('m\\:jc').attr('m:val'), 'center'); +}); + +test('formula-only Aspect instructions survive the real manual-code filter', async () => { + const html = ''; + assert.equal(hasCodebookManualInstruction(html), true); + const raw = [ + { + key: 'A', + name: 'Aspect', + scheme: JSON.stringify({ + version: '3.4', + variableCodings: [ + { + id: 'V', + sourceType: 'BASE', + manualInstruction: '', + codes: [{ id: 1, type: 'FULL_CREDIT', label: 'Code', score: 1, manualInstruction: html, ruleSets: [] }], + }, + ], + }), + }, + ]; + const data = CodebookGenerator.getCodebookData(raw, defaults, []); + assert.equal(data[0].variables.length, 1); + assert.equal(data[0].variables[0].codes.length, 1); +}); +test('newlines in a LaTeX attribute do not become HTML break tags', async () => { + const $ = await render( + '

    ', + ); + assert.equal($('m\\:oMath').length, 1); + assert.equal($('m\\:m').length, 1); + assert.ok(!$.root().text().includes('br')); +}); diff --git a/scripts/codebook-rich-matrix.test.mjs b/scripts/codebook-rich-matrix.test.mjs new file mode 100644 index 0000000..84f71e6 --- /dev/null +++ b/scripts/codebook-rich-matrix.test.mjs @@ -0,0 +1,111 @@ +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; +import { test } from 'node:test'; +import { load } from 'cheerio'; +import { CodebookDocxGenerator } from '../dist/ngx-coding-components/fesm2022/iqb-ngx-coding-components-codebook-generator.mjs'; +import { defaults } from './fixtures/codebook-synthetic.mjs'; +import { richFormatCases, aspectHtml } from './fixtures/codebook-rich-format-cases.mjs'; +import { docxParts, partHashes } from './codebook-docx-test-utils.mjs'; + +const require = createRequire(import.meta.url); +const CommonJs = require('../dist/ngx-coding-components/cjs/codebook-generator.cjs').CodebookDocxGenerator; + +for (const source of ['aspect', 'coding-components']) { + for (const showScore of [false, true]) { + test(`complete ${source} rich format matrix, score=${showScore}, ESM/CJS`, async () => { + const cases = richFormatCases().filter( + (entry) => + source === 'aspect' || !['inline-image', 'horizontal-rules', 'aspect-annotations'].includes(entry.group), + ); + const units = [ + { + key: 'FORMAT', + name: 'Synthetic acceptance matrix', + items: [], + missings: [], + variables: [ + { + id: 'V', + label: 'Formatting', + sourceType: 'BASE', + generalInstruction: '', + codes: cases.map((entry, index) => ({ + id: String(index + 1), + label: entry.group, + score: '1', + description: source === 'aspect' ? aspectHtml(entry.html) : entry.html, + })), + }, + ], + }, + ]; + const options = { ...defaults, showScore }; + const parts = await docxParts(await CodebookDocxGenerator.generateDocx(units, options)); + const commonParts = await docxParts(await CommonJs.generateDocx(units, options)); + assert.deepEqual(partHashes(parts), partHashes(commonParts)); + const $ = load(parts['word/document.xml'], { xml: true }); + const paragraphs = new Map(); + $('w\\:p').each((_, paragraph) => { + const id = $(paragraph) + .find('w\\:t') + .text() + .match(/FMT\d{4}/)?.[0]; + if (id) paragraphs.set(id, paragraph); + }); + assert.equal(paragraphs.size, cases.length); + let markCases = 0; + for (const entry of cases) { + const p = $(paragraphs.get(entry.id)); + const expected = entry.expected; + if (expected.align) { + assert.equal( + p.find('w\\:jc').attr('w:val'), + expected.align === 'justify' ? 'both' : expected.align, + entry.id, + ); + } + if (entry.group === 'marks-font-alignment') { + const run = p.find('w\\:r').filter((_, node) => $(node).find('w\\:t').text().includes(entry.id)); + assert.equal(run.find('w\\:sz').attr('w:val'), String(Math.round(expected.size * 1.5)), entry.id); + for (const [bit, tag] of ['b', 'i', 'u', 'strike'].entries()) { + assert.equal(!!run.find(`w\\:${tag}`).length, !!(expected.mask & (1 << bit)), `${entry.id} ${tag}`); + } + const vertical = expected.vertical ? (expected.vertical === 'sup' ? 'superscript' : 'subscript') : undefined; + assert.equal(run.find('w\\:vertAlign').attr('w:val'), vertical, entry.id); + assert.ok(run.text().includes('10\u00a0kg; 20\u202fcm; Abstand bleibt'), entry.id); + markCases += 1; + } else if (entry.group === 'format-boundaries') { + const run = (text) => p.find('w\\:r').filter((_, node) => $(node).children('w\\:t').text() === text); + assert.equal(run('FARBE').find('w\\:color').attr('w:val'), expected.hex, entry.id); + assert.equal(run('FARBE').find('w\\:sz').attr('w:val'), '36', entry.id); + assert.equal(run(' NORMAL ').find('w\\:color').attr('w:val'), '000000', entry.id); + assert.equal(run(' ENDE').find('w\\:shd').attr('w:fill'), 'FFFFFF', entry.id); + } else if (entry.group === 'paragraph-layout') { + assert.equal(p.find('w\\:ind').attr('w:start'), String(expected.indent * 300), entry.id); + assert.equal(p.find('w\\:ind').attr('w:hanging'), expected.hanging ? '300' : undefined, entry.id); + assert.equal(p.find('w\\:spacing').attr('w:after'), String(expected.margin * 15), entry.id); + } else if (entry.group === 'list-styles-depth') { + const marker = { + disc: '•', + circle: '○', + square: '▪', + decimal: '1.', + 'lower-latin': 'a.', + 'upper-latin': 'A.', + 'lower-roman': 'i.', + 'upper-roman': 'I.', + 'lower-greek': 'α.', + }; + assert.ok(p.find('w\\:t').text().startsWith(marker[expected.style]), entry.id); + assert.equal(p.find('w\\:r > w\\:tab').length, 1, entry.id); + assert.equal(p.find('w\\:ind').attr('w:start'), String(expected.depth * 600 + 100), entry.id); + } + } + assert.equal(markCases, 2496); + assert.equal($('m\\:oMath').length, 40); + assert.equal($('w\\:drawing').length, source === 'aspect' ? 4 : 3); + assert.equal($('wp\\:anchor').length, 2); + assert.doesNotMatch(parts['word/document.xml'], /HTML konnte nicht verarbeitet werden| /^\s+$/.test($(p).find('w\\:t').text())), false); if (save) { diff --git a/scripts/fixtures/codebook-rich-format-cases.mjs b/scripts/fixtures/codebook-rich-format-cases.mjs new file mode 100644 index 0000000..cc9a193 --- /dev/null +++ b/scripts/fixtures/codebook-rich-format-cases.mjs @@ -0,0 +1,118 @@ +import { image } from './codebook-synthetic.mjs'; +export const fontSizes = [8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 36]; +export const alignments = ['left', 'center', 'right', 'justify']; +export const listStyles = [ + 'disc', + 'circle', + 'square', + 'decimal', + 'lower-latin', + 'upper-latin', + 'lower-roman', + 'upper-roman', + 'lower-greek', +]; +const attribute = (value) => value.replaceAll('&', '&').replaceAll('"', '"').replaceAll('<', '<'); +export function richFormatCases() { + const cases = []; + let index = 0; + const add = (group, html, expected = {}) => { + const id = `FMT${String(++index).padStart(4, '0')}`; + cases.push({ id, group, html: html.replaceAll('CASE', id), expected }); + }; + for (let mask = 0; mask < 16; mask++) + for (const vertical of ['', 'sub', 'sup']) + for (const align of alignments) + for (const size of fontSizes) { + const tags = ['strong', 'em', 'u', 's'].filter((_, bit) => mask & (1 << bit)); + if (vertical) tags.push(vertical); + const text = 'CASE 10\u00a0kg; 20\u202fcm; Abstand bleibt'; + const marked = tags.reduce((s, t) => `<${t}>${s}`, text); + add( + 'marks-font-alignment', + `

    ${marked}

    `, + { mask, vertical, align, size }, + ); + } + for (const [value, hex] of [ + ['#234567', '234567'], + ['rgb(255, 0, 0)', 'FF0000'], + ['blue', '0000FF'], + ['#0a3', '00AA33'], + ]) + for (const align of alignments) { + add( + 'format-boundaries', + `

    CASE VOR FARBE NORMAL MARK ENDE

    `, + { hex, align }, + ); + } + for (const style of listStyles) + for (let depth = 1; depth <= 5; depth++) { + const tag = ['disc', 'circle', 'square'].includes(style) ? 'ul' : 'ol'; + let html = '

    CASE LISTE

    '; + for (let d = 0; d < depth; d++) html = `<${tag} style="list-style:${style}">
  • ${html}
  • `; + add('list-styles-depth', html, { style, depth }); + } + for (const align of alignments) + for (const margin of [0, 10, 20]) + for (let indent = 0; indent <= 4; indent++) + for (const hanging of [false, true]) { + add( + 'paragraph-layout', + `

    CASE ABSATZ

    `, + { align, margin, indent, hanging }, + ); + } + for (const level of [1, 2, 3, 4]) + for (const align of alignments) + add('headings', `CASE UEBERSCHRIFT`, { level, align }); + for (const float of ['none', 'left', 'right']) + add( + 'block-images', + `

    CASE VOR BILD

    NACH BILD

    `, + { float }, + ); + add( + 'inline-image', + `

    CASE VORNACH

    `, + ); + for (const latex of [ + 'x^2', + '\\frac{1}{2}', + '\\sqrt{x}', + 'aCASE DANACH

    `, + { latex, align }, + ); + } + add( + 'linebreaks-specials', + '

    CASE 10 kg;20 cm;FETT KURSIV
    ZEILE – ‑ ‘ ’ ‚ „ “ ” ‹ › « » µ ç Ç

    ABSATZ

    ', + ); + add( + 'blockquote', + '

    CASE ZITAT FETT

    VERSCHACHTELT

    ', + ); + add( + 'horizontal-rules', + '

    CASE VOR


    MITTE


    NACH

    ', + ); + return cases; +} +export function aspectHtml(html) { + return html.replace( + /<\/span>/g, + (_, latex) => ``, + ); +} diff --git a/scripts/fixtures/word-acceptance/README.md b/scripts/fixtures/word-acceptance/README.md index ecfb2b7..2d24c08 100644 --- a/scripts/fixtures/word-acceptance/README.md +++ b/scripts/fixtures/word-acceptance/README.md @@ -32,3 +32,11 @@ is `scripts/codebook-editor-matrix.test.mjs` (384 cases, each ESM and CommonJS). `kodierbox-browser.docx` is the corresponding actual browser download produced by a real Redis generation job in Kodierbox. Its JSON companion and every normalized DOCX part match the Studio browser download exactly. + +## Extended Aspect and rich-text acceptance + +`aspect-full-matrix.docx` and `cc-full-matrix.docx` contain 2,740 / 2,738 browser-authored cases. `aspect-feature-specimen.docx` and `cc-feature-specimen.docx` provide readable subsets covering every distinct feature. `toolbar-acceptance.docx` includes actual toolbar output plus 13 styled native formula sizes; `studio-reference-current.docx` includes the explicitly corrected 18pt legacy sample. + +The current regression corpus is `../codebook-rich-format-cases.mjs`. Execute `npm run build_cc && npm run test:codebook`. The full matrix runs through both ESM and CommonJS and both score layouts. The existing independent original-Studio fixtures are retained; only the strictly asserted font-size correction and equivalent highlight hex spelling are normalized by the legacy comparison. + +These six documents were rendered in Microsoft Office Web Viewer / Word: 261 captured pages, with each actual page number independently checked in its footer. The final bidirectional editor interchange produces the same complete normalized Word parts as the rendered matrix. See `docs/codebook-aspect-acceptance-2026-10-13.md`. The earlier fixture documents and package hashes above remain historical evidence. From b1483e4524ac295d682d60eb6a31c154575c5687 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Wed, 14 Oct 2026 00:06:31 +0200 Subject: [PATCH 27/36] docs: finalize Word acceptance evidence --- scripts/fixtures/word-acceptance/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/fixtures/word-acceptance/README.md b/scripts/fixtures/word-acceptance/README.md index 2d24c08..d2ac2b0 100644 --- a/scripts/fixtures/word-acceptance/README.md +++ b/scripts/fixtures/word-acceptance/README.md @@ -39,4 +39,6 @@ normalized DOCX part match the Studio browser download exactly. The current regression corpus is `../codebook-rich-format-cases.mjs`. Execute `npm run build_cc && npm run test:codebook`. The full matrix runs through both ESM and CommonJS and both score layouts. The existing independent original-Studio fixtures are retained; only the strictly asserted font-size correction and equivalent highlight hex spelling are normalized by the legacy comparison. -These six documents were rendered in Microsoft Office Web Viewer / Word: 261 captured pages, with each actual page number independently checked in its footer. The final bidirectional editor interchange produces the same complete normalized Word parts as the rendered matrix. See `docs/codebook-aspect-acceptance-2026-10-13.md`. The earlier fixture documents and package hashes above remain historical evidence. +These seven current documents (including `formula-compound-fixed.docx`) were rendered in Microsoft Office Web Viewer / Word: 262 captured pages, with each actual page number independently checked in its footer. The final bidirectional editor interchange produces the same complete normalized Word parts as the rendered matrix. See `docs/codebook-aspect-acceptance-2026-10-13.md`. The earlier fixture documents and package hashes above remain historical evidence. + +`formula-compound-before.docx` is a historical failing reference for wide accents and black control symbols. Its one page is retained separately and is not included in the 262 passing pages. The corrected counterpart includes a true wide underline as well. From 13a88621ef7e2c6f0317e53ac43276735472d2a7 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:41:50 +0200 Subject: [PATCH 28/36] fix: retain instruction text around invalid inline images --- package.json | 2 +- .../codebook-docx-generator.ts | 9 ++++- scripts/codebook-inline-images.test.mjs | 38 +++++++++++++++++++ scripts/fixtures/codebook-inline-images.mjs | 31 +++++++++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 scripts/codebook-inline-images.test.mjs create mode 100644 scripts/fixtures/codebook-inline-images.mjs diff --git a/package.json b/package.json index df1e38b..1b98060 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test:schemer:coverage": "ng test schemer --watch=false --browsers=ChromeHeadless --code-coverage", "test:coverage": "npm run test:cc:coverage && npm run test:schemer:coverage", "test:scheme": "node --test scripts/validate-coding-scheme.test.js", - "test:codebook": "node --test scripts/codebook-contract.test.mjs scripts/codebook-matrix.test.mjs scripts/codebook-whitespace.test.mjs scripts/codebook-editor-matrix.test.mjs scripts/codebook-rich-layout.test.mjs scripts/codebook-rich-matrix.test.mjs scripts/codebook-docx-test-utils.test.mjs", + "test:codebook": "node --test scripts/codebook-contract.test.mjs scripts/codebook-matrix.test.mjs scripts/codebook-whitespace.test.mjs scripts/codebook-editor-matrix.test.mjs scripts/codebook-rich-layout.test.mjs scripts/codebook-rich-matrix.test.mjs scripts/codebook-docx-test-utils.test.mjs scripts/codebook-inline-images.test.mjs", "test:schemer:responsive": "playwright test --config playwright.schemer.config.cjs" }, "dependencies": { diff --git a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts index e4b2b2d..b76226a 100644 --- a/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts +++ b/projects/ngx-coding-components/codebook-generator/codebook-docx-generator.ts @@ -453,8 +453,13 @@ export class CodebookDocxGenerator { return; } if (tagName === 'img') { - const fontPixels = (CodebookDocxGenerator.getFontSize(size) * 2) / 3; - children.push(CodebookDocxGenerator.createImageRun(element, imageWidth, fontPixels)); + try { + const fontPixels = (CodebookDocxGenerator.getFontSize(size) * 2) / 3; + children.push(CodebookDocxGenerator.createImageRun(element, imageWidth, fontPixels)); + } catch { + // An invalid image must not discard the surrounding instruction paragraph. + children.push(new TextRun({ text: 'HTML konnte nicht verarbeitet werden.', ...runStyle })); + } return; } if (tagName === 'br') { diff --git a/scripts/codebook-inline-images.test.mjs b/scripts/codebook-inline-images.test.mjs new file mode 100644 index 0000000..f1c0651 --- /dev/null +++ b/scripts/codebook-inline-images.test.mjs @@ -0,0 +1,38 @@ +import assert from 'node:assert/strict'; +import { createRequire } from 'node:module'; +import { test } from 'node:test'; +import { load } from 'cheerio'; +import { CodebookGenerator } from '../dist/ngx-coding-components/fesm2022/iqb-ngx-coding-components-codebook-generator.mjs'; +import { defaults } from './fixtures/codebook-synthetic.mjs'; +import { inlineImageCases, inlineImageUnits } from './fixtures/codebook-inline-images.mjs'; +import { docxParts } from './codebook-docx-test-utils.mjs'; + +const require = createRequire(import.meta.url); +const commonjs = require('../dist/ngx-coding-components/cjs/codebook-generator.cjs'); +const errorText = 'HTML konnte nicht verarbeitet werden.'; + +for (const [entryPoint, generator] of [['ESM', CodebookGenerator], ['CommonJS', commonjs.CodebookGenerator]]) { + for (const location of ['general', 'code']) for (const showScore of [false, true]) { + for (const entry of inlineImageCases) { + test(`${entryPoint}: ${entry.id} inline image preserves ${location} instruction, score=${showScore}`, async () => { + const parts = await docxParts(await generator.generateCodebook(inlineImageUnits(entry, location), + { ...defaults, showScore }, [])); + const $ = load(parts['word/document.xml'], { xml: true }); + const paragraphs = $('w\\:p').toArray().filter(p => $(p).find('w\\:t').text().startsWith('VOR')); + assert.equal(paragraphs.length, 1, 'the instruction paragraph must survive the failed image'); + const p = $(paragraphs[0]); + assert.equal(p.find('w\\:t').text(), `VOR${errorText.repeat(entry.errors)}NACH`); + assert.equal(p.closest('w\\:tbl').length, location === 'code' ? 1 : 0); + const run = text => p.find('w\\:r').filter((_, node) => $(node).children('w\\:t').text() === text); + assert.equal(run('VOR').find('w\\:b').length, 1, 'preceding text keeps its bold mark'); + assert.equal(run('NACH').find('w\\:color').attr('w:val'), 'FF0000'); + assert.equal(run('NACH').find('w\\:sz').attr('w:val'), '36'); + assert.equal($('w\\:t').toArray().filter(node => $(node).text() === 'WEITER').length, 1); + assert.equal(p.find('w\\:drawing').length, entry.images, 'a later valid image still renders once'); + assert.equal(p.find('m\\:oMath').length, entry.formulas, 'a later formula still renders once'); + assert.equal($('w\\:drawing').length, entry.images); + assert.equal(Object.keys(parts).filter(name => name.startsWith('word/media/')).length, entry.images); + }); + } + } +} diff --git a/scripts/fixtures/codebook-inline-images.mjs b/scripts/fixtures/codebook-inline-images.mjs new file mode 100644 index 0000000..78d9350 --- /dev/null +++ b/scripts/fixtures/codebook-inline-images.mjs @@ -0,0 +1,31 @@ +import { image } from './codebook-synthetic.mjs'; + +// Invented error cases; no task content or user images are used. +export const inlineImageCases = [ + { id: 'corrupt', imageHtml: '' }, + { id: 'empty', imageHtml: '' }, + { id: 'missing', imageHtml: '' }, + { id: 'unsupported', imageHtml: '' }, +].map(entry => ({ ...entry, errors: 1, images: 0, formulas: 0 })); +inlineImageCases.push({ + id: 'mixed', + imageHtml: `` + + ``, + errors: 2, images: 1, formulas: 1, +}); + +export function inlineImageHtml(entry) { + return `

    VOR${entry.imageHtml}NACH

    ` + + '

    WEITER

    '; +} + +export function inlineImageUnits(entry, location) { + const html = inlineImageHtml(entry); + return [{ id: 101, key: 'INLINE_IMAGE', name: 'Synthetischer Bildfehler', scheme: JSON.stringify({ + version: '3.4', variableCodings: [{ id: 'V', sourceType: 'BASE', + manualInstruction: location === 'general' ? html : '', + codes: [{ id: 0, type: 'FULL_CREDIT', label: 'Code', score: 0, + manualInstruction: location === 'code' ? html : 'CODE_UNCHANGED', ruleSets: [] }], + }], + }) }]; +} From 74036f3a1920b9895d174b8723d7a64ba8b9b83c Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Thu, 15 Oct 2026 08:47:44 +0200 Subject: [PATCH 29/36] test: verify installed codebook files and image error exports --- ...ge-installation-comparison-2026-10-15.json | 18 +++++ docs/codebook-inline-images-2026-10-15.md | 69 +++++++++++++++++ docs/shared-codebook.md | 8 +- scripts/compare-codebook-installations.mjs | 77 ++++++++++++++++--- 4 files changed, 161 insertions(+), 11 deletions(-) create mode 100644 docs/codebook-inline-image-installation-comparison-2026-10-15.json create mode 100644 docs/codebook-inline-images-2026-10-15.md diff --git a/docs/codebook-inline-image-installation-comparison-2026-10-15.json b/docs/codebook-inline-image-installation-comparison-2026-10-15.json new file mode 100644 index 0000000..53cd633 --- /dev/null +++ b/docs/codebook-inline-image-installation-comparison-2026-10-15.json @@ -0,0 +1,18 @@ +{ + "hostExportPairs": 4694, + "optionCombinations": 768, + "missingCases": 3, + "formats": 2, + "originalStudioWhitespaceCases": 20, + "inlineImageCases": 5, + "inlineImageExportPairs": 40, + "identicalJson": true, + "identicalAllDocxParts": true, + "artifactIntegrity": "sha512-lWKtd2Kpd94e48pefxidp4XXwe/AE/3KVRqQQpJAZxW+acbT+CuJDY0jEJS1pQ9MG6h5lQMZTa7KtiCTGd+yJQ==", + "identicalInstalledPackageFiles": true, + "installedPackageFiles": 20, + "realHostAdapters": true, + "inMemoryRepositories": true, + "httpBrowserRedis": false, + "windowsWord": false +} \ No newline at end of file diff --git a/docs/codebook-inline-images-2026-10-15.md b/docs/codebook-inline-images-2026-10-15.md new file mode 100644 index 0000000..1fbb01b --- /dev/null +++ b/docs/codebook-inline-images-2026-10-15.md @@ -0,0 +1,69 @@ +# Inlinebild-Korrektur und Installationsvergleich vom 15.10.2026 + +## Korrektur + +Ein defektes Inlinebild konnte den gesamten umgebenden Instruktionsabsatz durch +„HTML konnte nicht verarbeitet werden.“ ersetzen. Der Fehler wird jetzt direkt +am Bild behandelt: Der Hinweis steht an seiner Position, Text und Formatierung +vor und nach dem Bild sowie folgende Absätze bleiben erhalten. Spätere gültige +Bilder und Formeln werden weiterhin genau einmal ausgegeben. Gültige Dokumente, +Filter, JSON-Inhalte, UI und Jobverarbeitung werden nicht geändert. + +## Regressionstests + +40 neue Tests prüfen fünf erfundene Fälle (defekte Bildbytes, leeres src, +fehlendes src, nicht unterstützte Bildbytes und mehrere defekte Bilder vor +einem gültigen Bild und einer Formel). Alle Fälle werden als allgemeine und +Code-Instruktion, mit und ohne Score-Spalte, über ESM und CommonJS exportiert. +Geprüft werden vollständiger Absatztext, Quellreihenfolge, Fettmarkierung, Farbe, +Schriftgröße, Folgeabsatz, Bildmedien und native Formeln. Alle 40 Tests schlugen +vor der Korrektur wegen des fehlenden Absatzes fehl und bestehen danach. + +Studio und Kodierbox ergänzen außerdem jeweils zwei Tests gegen das tatsächlich +installierte Paket: allgemeine und Code-Instruktionen in beiden Score-Layouts. + +- Bibliothek frisch gebaut; 1.285 Generator-Tests ohne übersprungene Tests bestanden. +- 526 Angular-Tests bestanden; Generator-Lint ohne Fehler, sechs bestehende Warnungen. +- Studio: 15 API-Tests und 66 betroffene Frontend-Tests bestanden; API-Lint ohne Fehler. +- Kodierbox: gesamte Backend-Suite mit 2.635 bestandenen und 17 übersprungenen Tests; + 60 betroffene Frontend-Tests bestanden; Backend-Lint ohne Fehler. +- Bibliotheksbuild sowie beide Frontend- und beide Backend/API-Builds bestanden; + Frontend und Backend wurden getrennt und nacheinander geprüft. +- Die 32 ursprünglichen Studio-Rohschema-Referenzen und 20 Leerzeichenreferenzen + wurden nicht neu erzeugt und bestehen weiterhin. Die bereits dokumentierte, + gezielt normalisierte 18pt-Korrektur bleibt unverändert. + +## Gleiches installiertes Artefakt + +Beide Anwendungen verwenden denselben neuen 4.1.0-Tarball. SHA256: +`1340db41e6493cca3d4395426c14f156319c3bbbf0b81d01f84c233a56a5091a`. +Root-Locks und Studios API-Runtime-Lock enthalten dieselbe neue Paketprüfsumme; +sonstige Abhängigkeitsversionen bleiben unverändert. + +Der Vergleich prüft vor dem Export die Tarball-Bytes gegen die Lock-Prüfsumme +und alle 20 ausgelieferten Paketdateien gegen den frischen Build. Das verhindert, +dass eine alte npm-Installation trotz aktualisiertem Lock unbemerkt verglichen wird. + +4.694 Exportpaare bestehen: die bisherigen 4.654 plus 40 zusätzliche Bildfehler- +Paare (fünf Fälle, zwei Instruktionsorte, zwei Score-Layouts, JSON und DOCX). +JSON wird strukturell, DOCX über alle normalisierten Dokumentbestandteile inklusive +Bildern, Beziehungen, Styles und Fußzeile verglichen. Die neuen Fälle prüfen +zusätzlich erwartete Inhalte, Code 0 und optionale Scores ausdrücklich. + +Der maschinenlesbare Nachweis liegt in der Bibliothek unter +`docs/codebook-inline-image-installation-comparison-2026-10-15.json`. + +## Nachweisgrenzen + +Die echten Anwendungsadapter laufen in diesem Vergleich mit Datenhaltung im Speicher. +HTTP, Browser, PostgreSQL und Redis wurden für diesen Fix nicht erneut geprüft. +Die frühere Word-Web-Abnahme gilt für den damaligen Paketstand; für den neuen +Tarball wurde kein neuer Microsoft-Renderinglauf ausgeführt. Desktop-Word unter +Windows bleibt offen. Es wurde kein Paket veröffentlicht, gemergt oder ausgerollt. + +## Wiederholung + +In coding-components: `npm run build_cc`, danach `npm run test:codebook` und +`npm run test:cc`. Den gepackten Tarball in beide Anwendungen installieren und +`node scripts/compare-codebook-installations.mjs STUDIO_ROOT BOX_ROOT OUTPUT_DIR` +ausführen. Frontend- und Backend-Prüfungen getrennt und nacheinander ausführen. diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md index ce6b390..1661d28 100644 --- a/docs/shared-codebook.md +++ b/docs/shared-codebook.md @@ -1,8 +1,12 @@ # Gemeinsames Codebook: Integrationsvertrag -## Aktuelle Format- und Aspect-Abnahme +## Aktuelle Inlinebild-Korrektur -Die [Browser- und Word-Abnahme vom 13.10.2026](codebook-aspect-acceptance-2026-10-13.md) ergänzt die frühere Leerzeichen-Abnahme um bestätigte Formatierungsfixes und den bidirektionalen Formel-Austausch. Aktuelles Paket-SHA256: `0e355c3896799ae765cb6e90b00147da0359c56f5fdc90d36b7caf40fbe59ab8`. 1.245 Generator-Tests, 4.654 installierte Exportpaare und 262 tatsächliche Microsoft-Word-Web-Seiten sind geprüft. Die explizite 18pt-Schriftgröße des alten `Farbe`-Referenzfalls wird bewusst korrigiert; alle übrigen alten Dokumentbestandteile bleiben streng verglichen. Angaben zu früheren Paketständen weiter unten sind historische Nachweise. +Die [Prüfung vom 15.10.2026](codebook-inline-images-2026-10-15.md) behebt Textverlust bei defekten Inlinebildern. 1.285 Generator-Tests und 4.694 installierte Exportpaare bestehen. Beide Anwendungen verwenden den Tarball mit SHA256 `1340db41e6493cca3d4395426c14f156319c3bbbf0b81d01f84c233a56a5091a`. Eine neue Word-Abnahme wurde für diesen Paketstand nicht ausgeführt. + +## Format- und Aspect-Abnahme vom 13.10.2026 + +Die [Browser- und Word-Abnahme vom 13.10.2026](codebook-aspect-acceptance-2026-10-13.md) ergänzt die frühere Leerzeichen-Abnahme um bestätigte Formatierungsfixes und den bidirektionalen Formel-Austausch. Damals geprüftes Paket-SHA256: `0e355c3896799ae765cb6e90b00147da0359c56f5fdc90d36b7caf40fbe59ab8`. 1.245 Generator-Tests, 4.654 installierte Exportpaare und 262 tatsächliche Microsoft-Word-Web-Seiten sind geprüft. Die explizite 18pt-Schriftgröße des alten `Farbe`-Referenzfalls wird bewusst korrigiert; alle übrigen alten Dokumentbestandteile bleiben streng verglichen. Angaben zu früheren Paketständen weiter unten sind historische Nachweise. Studio ist die Referenz für Bedienung und Dokumentlayout. Die drei Einstiegspunkte `codebook-models`, `codebook-generator` und `codebook-export` trennen Daten, diff --git a/scripts/compare-codebook-installations.mjs b/scripts/compare-codebook-installations.mjs index 1e87b74..6cc6bec 100644 --- a/scripts/compare-codebook-installations.mjs +++ b/scripts/compare-codebook-installations.mjs @@ -1,12 +1,15 @@ // Runs real host preparation and adapters against in-memory persistence boundaries. // Does not replace an HTTP/browser/Redis acceptance run. import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; import { createRequire, Module } from 'node:module'; -import { readFile, writeFile, mkdir } from 'node:fs/promises'; +import { readFile, writeFile, mkdir, readdir } from 'node:fs/promises'; import { resolve, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; import { load } from 'cheerio'; import { optionMatrix, selectionCorpus, missingCases, expectedSelection, defaults, referenceCorpus, referenceOptions } from './fixtures/codebook-synthetic.mjs'; import { whitespaceReferenceCases, whitespaceReferenceUnits } from './fixtures/codebook-whitespace.mjs'; +import { inlineImageCases, inlineImageHtml, inlineImageUnits } from './fixtures/codebook-inline-images.mjs'; import { docxParts, partHashes, originalStudioCompatibleParts } from './codebook-docx-test-utils.mjs'; const [studioPath, boxPath, outputPath] = process.argv.slice(2); @@ -30,6 +33,36 @@ const studio = resolve(studioPath); const box = resolve(boxPath); const output = resolve(outputPath); await mkdir(output, { recursive: true }); +// npm may retain old files when a local tarball keeps its name and version. +// Verify actual installations before comparing exports, not just lockfile metadata. +async function packageFiles(directory, prefix = '') { + const files = []; + for (const entry of await readdir(directory, { withFileTypes: true })) { + // ng-packagr excludes development-only nested manifests via .npmignore. + if (entry.name === '.npmignore' || (prefix && entry.name === 'package.json')) continue; + const relative = join(prefix, entry.name); + if (entry.isDirectory()) files.push(...await packageFiles(join(directory, entry.name), relative)); + else files.push(relative); + } + return files; +} +const builtPackage = fileURLToPath(new URL('../dist/ngx-coding-components', import.meta.url)); +const builtFiles = await packageFiles(builtPackage); +const artifacts = []; +for (const root of [studio, box]) { + const lock = JSON.parse(await readFile(join(root, 'package-lock.json'), 'utf8')); + const dependency = lock.packages['node_modules/@iqb/ngx-coding-components']; + assert.ok(dependency.resolved.startsWith('file:vendor/'), 'comparison requires the packed library artifact'); + const tarball = await readFile(join(root, dependency.resolved.slice(5))); + const integrity = `sha512-${createHash('sha512').update(tarball).digest('base64')}`; + assert.equal(integrity, dependency.integrity, 'lock integrity must match the actual tarball'); + artifacts.push(integrity); + for (const file of builtFiles) { + assert.deepEqual(await readFile(join(root, 'node_modules/@iqb/ngx-coding-components', file)), + await readFile(join(builtPackage, file)), `stale or different installed package file: ${root}/${file}`); + } +} +assert.equal(artifacts[0], artifacts[1], 'both installed artifacts must have the same integrity'); const Studio = (await hostModule(studio, 'apps/api/src/app/classes/download-workspaces.class.ts', { '@studio-lite/shared-code': { isCurrentFromOrder: order => order === 0 } // unrelated metadata reports, never called here })).DownloadWorkspacesClass; @@ -64,7 +97,7 @@ service.logger = { log() {}, warn() {}, error(...args) { throw new Error(args.jo let currentMissings = []; let comparisons = 0; const wordCases = []; -async function compare(options, missings, save = false, suffix = '', reference = null) { +async function compare(options, missings, save = false, suffix = '', reference = null, verify = null) { currentMissings = missings; const current = { ...options, missingsProfile: missings.length ? 'Synthetic profile' : '' }; const ids = currentUnits.map(unit => unit.id); @@ -75,7 +108,7 @@ async function compare(options, missings, save = false, suffix = '', reference = if (current.exportFormat === 'json') { assert.deepEqual(JSON.parse(a.toString()), JSON.parse(b.toString()), 'Studio preparation vs Box service + adapter'); if (reference) assert.deepEqual(JSON.parse(a.toString()), reference.json, 'installed host vs original Studio JSON'); - else assert.deepEqual(JSON.parse(a.toString())[0].variables.map(({ id, codes }) => ({ id, codes: codes.map(c => c.id) })), expectedSelection(rows, options)); + else if (!verify) assert.deepEqual(JSON.parse(a.toString())[0].variables.map(({ id, codes }) => ({ id, codes: codes.map(c => c.id) })), expectedSelection(rows, options)); } else { const aParts = await docxParts(a); const bParts = await docxParts(b); @@ -90,6 +123,7 @@ async function compare(options, missings, save = false, suffix = '', reference = variableHeadings: $('w\\:pStyle').filter((_, e) => $(e).attr('w:val') === 'Heading2').length }); } } + if (verify) await verify(a, current); if (save) { await writeFile(join(output, `studio${suffix}.${current.exportFormat}`), a); await writeFile(join(output, `kodierbox${suffix}.${current.exportFormat}`), b); @@ -119,15 +153,40 @@ for (const entry of whitespaceReferenceCases()) { for (const exportFormat of ['json', 'docx']) await compare({ ...entry.options, exportFormat }, [], false, '', reference); } assert.equal(whitespaceCases, whitespaceReferences.length); +let inlineImageExportPairs = 0; +for (const entry of inlineImageCases) for (const location of ['general', 'code']) { + currentUnits = inlineImageUnits(entry, location); + for (const showScore of [false, true]) for (const exportFormat of ['json', 'docx']) { + await compare({ ...defaults, showScore, exportFormat }, [], entry.id === 'mixed', + `-inline-${entry.id}-${location}-${showScore ? 'scores' : 'no-scores'}`, null, async (buffer, options) => { + if (options.exportFormat === 'json') { + const [variable] = JSON.parse(buffer.toString())[0].variables; + assert.equal(variable.generalInstruction, location === 'general' ? inlineImageHtml(entry) : ''); + assert.equal(variable.codes[0].description, location === 'code' ? inlineImageHtml(entry) : 'CODE_UNCHANGED'); + assert.equal(variable.codes[0].id, '0'); + if (showScore) assert.equal(variable.codes[0].score, '0'); + else assert.equal('score' in variable.codes[0], false); + } else { + const parts = await docxParts(buffer); + const $ = load(parts['word/document.xml'], { xml: true }); + const paragraphs = $('w\\:p').toArray().filter(p => $(p).find('w\\:t').text().startsWith('VOR')); + assert.equal(paragraphs.length, 1, 'installed host must retain surrounding instruction text'); + const p = $(paragraphs[0]); + assert.equal(p.find('w\\:t').text(), `VOR${'HTML konnte nicht verarbeitet werden.'.repeat(entry.errors)}NACH`); + assert.equal(p.closest('w\\:tbl').length, location === 'code' ? 1 : 0); + assert.equal($('w\\:t').toArray().filter(node => $(node).text() === 'WEITER').length, 1); + assert.equal($('w\\:drawing').length, entry.images); + assert.equal(p.find('m\\:oMath').length, entry.formulas); + } + }); + inlineImageExportPairs += 1; + } +} await writeFile(join(output, 'word-expectations.json'), JSON.stringify({ cases: wordCases }, null, 2)); -const roots = [studio, box]; -const artifacts = await Promise.all(roots.map(async root => { - const lock = JSON.parse(await readFile(join(root, 'package-lock.json'), 'utf8')); - return lock.packages['node_modules/@iqb/ngx-coding-components'].integrity; -})); -assert.equal(artifacts[0], artifacts[1], 'both installed artifacts must have the same integrity'); await writeFile(join(output, 'installation-comparison.json'), JSON.stringify({ hostExportPairs: comparisons, optionCombinations: 768, missingCases: 3, formats: 2, originalStudioWhitespaceCases: whitespaceCases, + inlineImageCases: inlineImageCases.length, inlineImageExportPairs, identicalJson: true, identicalAllDocxParts: true, artifactIntegrity: artifacts[0], + identicalInstalledPackageFiles: true, installedPackageFiles: builtFiles.length, realHostAdapters: true, inMemoryRepositories: true, httpBrowserRedis: false, windowsWord: false }, null, 2)); console.log(`PASS: ${comparisons} export pairs through real Studio and Box preparation/adapters.`); From b5e837984c4f8c8f7a0e17626847643e447b98a2 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:30:50 +0200 Subject: [PATCH 30/36] Use host Responses 5.3 peer and cover Schemer compatibility --- package-lock.json | 8 +-- package.json | 2 +- .../ngx-coding-components/ng-package.json | 3 +- projects/ngx-coding-components/package.json | 6 +- .../services/responses-compatibility.spec.ts | 65 +++++++++++++++++++ 5 files changed, 74 insertions(+), 10 deletions(-) create mode 100644 projects/ngx-coding-components/src/lib/services/responses-compatibility.spec.ts diff --git a/package-lock.json b/package-lock.json index 3aa994a..0ad1534 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "@angular/platform-browser-dynamic": "^21.2.23", "@angular/router": "^21.2.23", "@iqb/mathlive": "^0.4.1", - "@iqb/responses": "^5.2.2", + "@iqb/responses": "5.3.1", "@iqbspecs/coding-scheme": "^3.4.1", "@iqbspecs/response": "^2.0.0", "@iqbspecs/variable-info": "^1.3.0", @@ -3799,9 +3799,9 @@ } }, "node_modules/@iqb/responses": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@iqb/responses/-/responses-5.2.2.tgz", - "integrity": "sha512-bHv1LHLVlkyL6rvlQgBedgi5lVFwNKEQaijJAw1HWUmYvKfV2+tZuU/+JvrJ3oVi42frcYF6Z5p7QldR30rx8A==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@iqb/responses/-/responses-5.3.1.tgz", + "integrity": "sha512-pP5NL1OC2NupC07sg/IYFYFtOjbAVusKL0XT9yt78LB13pfZTq0EuxZBMRe6ITctBlVw9sf3alqN0W2w01/lVg==", "license": "CC0 1.0", "dependencies": { "@iqbspecs/coding-scheme": "^3.4.1", diff --git a/package.json b/package.json index 1b98060..c7bfe77 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@angular/platform-browser-dynamic": "^21.2.23", "@angular/router": "^21.2.23", "@iqb/mathlive": "^0.4.1", - "@iqb/responses": "^5.2.2", + "@iqb/responses": "5.3.1", "@iqbspecs/coding-scheme": "^3.4.1", "@iqbspecs/response": "^2.0.0", "@iqbspecs/variable-info": "^1.3.0", diff --git a/projects/ngx-coding-components/ng-package.json b/projects/ngx-coding-components/ng-package.json index 5b04d68..08988e4 100644 --- a/projects/ngx-coding-components/ng-package.json +++ b/projects/ngx-coding-components/ng-package.json @@ -8,8 +8,7 @@ "cheerio", "buffer", "image-size", - "mathml2omml", - "@iqb/responses" + "mathml2omml" ], "lib": { "entryFile": "src/public-api.ts" diff --git a/projects/ngx-coding-components/package.json b/projects/ngx-coding-components/package.json index 9097043..5cc1834 100644 --- a/projects/ngx-coding-components/package.json +++ b/projects/ngx-coding-components/package.json @@ -60,7 +60,8 @@ "ngx-tiptap": "^12.0.0", "prosemirror-state": "^1.3.4", "rxjs": "~7.8.0", - "zone.js": "~0.15.0" + "zone.js": "~0.15.0", + "@iqb/responses": "^5.3.0" }, "devDependencies": { "@angular-devkit/build-angular": "^20.0.4", @@ -76,8 +77,7 @@ "cheerio": "1.1.2", "buffer": "6.0.3", "image-size": "1.2.1", - "mathml2omml": "0.5.0", - "@iqb/responses": "5.2.2" + "mathml2omml": "0.5.0" }, "sideEffects": false, "typesVersions": { diff --git a/projects/ngx-coding-components/src/lib/services/responses-compatibility.spec.ts b/projects/ngx-coding-components/src/lib/services/responses-compatibility.spec.ts new file mode 100644 index 0000000..5f046f7 --- /dev/null +++ b/projects/ngx-coding-components/src/lib/services/responses-compatibility.spec.ts @@ -0,0 +1,65 @@ +import { ToTextFactory } from '@iqb/responses'; +import { CodingScheme, VariableCodingData } from '@iqbspecs/coding-scheme'; +import { VariableInfo } from '@iqbspecs/variable-info/variable-info.interface'; +import { validateCodingScheme } from './coding-scheme-validation'; +import { SchemerService } from './schemer.service'; + +// Player payloads may use VariableInfo 2.x spelling while the public TS +// interface still describes 1.x. Keep this real serialization boundary. +const playerVariable = (type: string, format = ''): VariableInfo => JSON.parse(JSON.stringify({ + id: 'V', type, format, multiple: false, nullable: false, values: [], valuePositionLabels: [] +})); +const noValueCoding: VariableCodingData = { id: 'V', sourceType: 'BASE_NO_VALUE', codes: [] }; + +describe('Responses compatibility through real Schemer and validation imports', () => { + ['no-value', 'NO_VALUE'].forEach(type => { + it(`accepts ${type} for BASE_NO_VALUE without a false blocking source error`, () => { + expect(validateCodingScheme([playerVariable(type)], [noValueCoding])).toEqual([]); + }); + }); + + [ + ['string', 'STRING'], ['integer', 'INTEGER'], ['number', 'NUMBER'], ['boolean', 'BOOLEAN'], + ['attachment', 'ATTACHMENT'], ['json', 'JSON'], ['no-value', 'NO_VALUE'] + ].forEach(([legacy, current]) => { + it(`keeps the same recognized type description for ${legacy} and ${current}`, () => { + const actual = ToTextFactory.varInfoAsText(playerVariable(current)); + expect(actual).toEqual(ToTextFactory.varInfoAsText(playerVariable(legacy))); + expect(actual.join(' ')).not.toContain('unbekannt'); + }); + }); + + [ + ['text-selection', 'TEXT_SELECTION'], ['image', 'IMAGE'], ['capture-image', 'CAPTURE_IMAGE'], + ['audio', 'AUDIO'], ['ggb-file', 'GGB_FILE'], ['ggb-variable', 'GGB_VARIABLE'], + ['non-negative', 'NON_NEGATIVE'], ['latex', 'LATEX'], ['math-ml', 'MATH_ML'], + ['math-table', 'MATH_TABLE'], ['math-text-mix', 'MATH_TEXT_MIX'] + ].forEach(([legacy, current]) => { + it(`keeps the same recognized format description for ${legacy} and ${current}`, () => { + const actual = ToTextFactory.varInfoAsText(playerVariable('STRING', current)); + expect(actual).toEqual(ToTextFactory.varInfoAsText(playerVariable('string', legacy))); + expect(actual.join(' ')).not.toContain('unbekannt'); + }); + }); + + [ + ['STRING', 'CHEM_ELEMENT_SYMBOLS', 'Chemische Elementsymbole'], + ['JSON', 'CHEM_MOLECULE', 'Molekül mit Bild'] + ].forEach(([type, format, text]) => { + it(`recognizes the new chemistry format ${format}`, () => { + expect(ToTextFactory.varInfoAsText(playerVariable(type, format)).join(' ')).toContain(text); + }); + }); + + it('still reports an actual incompatible source instead of ignoring source validation', () => { + expect(validateCodingScheme([playerVariable('STRING')], [noValueCoding])) + .toContain(jasmine.objectContaining({ type: 'INVALID_SOURCE', breaking: true, variableId: 'V' })); + }); + + it('uses current VariableInfo spelling in the real SchemerService without mocking the Responses factory', () => { + const service = new SchemerService(); + service.setVarList([playerVariable('NO_VALUE')]); + service.setCodingScheme(new CodingScheme(JSON.stringify({ version: '3.4', variableCodings: [noValueCoding] }))); + expect(service.getCodingProblemsForVarCoding(noValueCoding)).toEqual([]); + }); +}); From 8eccdbba43c710846f59f2830f712a933073a5d5 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:31:03 +0200 Subject: [PATCH 31/36] Verify installed Responses and refresh codebook comparison evidence --- docs/codebook-responses-2026-10-15.md | 120 ++++++++++++++++++ ...es-installation-comparison-2026-10-15.json | 21 +++ docs/shared-codebook.md | 26 +++- scripts/compare-codebook-installations.mjs | 17 +++ 4 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 docs/codebook-responses-2026-10-15.md create mode 100644 docs/codebook-responses-installation-comparison-2026-10-15.json diff --git a/docs/codebook-responses-2026-10-15.md b/docs/codebook-responses-2026-10-15.md new file mode 100644 index 0000000..95349e4 --- /dev/null +++ b/docs/codebook-responses-2026-10-15.md @@ -0,0 +1,120 @@ +# Responses-Aktualisierung und Codebook-Prüfung vom 15.10.2026 + +## Versions- und Laufzeitvertrag + +`npm view @iqb/responses version` lieferte beim Abgleich 5.3.1. Diese aktuelle +Patchversion der gewünschten 5.3-Reihe ist in allen drei Root-Manifests exakt +festgelegt. Studios API-Runtime-Manifest und -Lock verwenden ebenfalls 5.3.1. +Die gemeinsame Bibliothek bezieht Responses als Peer mit `^5.3.0`, statt eine +private 5.2.2-Version für alle Bibliotheksfunktionen mitzuliefern. + +Damit lösen Schemer, Validierung und Codebook-Regeltextkonvertierung in beiden +Anwendungen dieselbe Responses-Installation wie ihr Host auf. Dies wird am +installierten Paket geprüft, nicht aus den Manifests angenommen. Der Generator +bleibt Angular-frei; Jobverarbeitung und Polling bleiben in der Kodierbox. + +Studio wird von 5.3.0 auf 5.3.1 aktualisiert, die Kodierbox-Kodierengine von 5.2.0 +auf 5.3.1. Kodierboxs Antwort- und Persistenzvertrag bleibt bei +`@iqbspecs/response` 1.5.1, einschließlich CODE_SELECTION_PENDING und alter +`codes`-Listen. Responses selbst benötigt intern Antwortformat 2.0. Der private +Typ für die Verarbeitung in CodingProcessService wird daher aus dem echten +Engine-Parameter abgeleitet. Die bestehende numerische Datenbank-Statusübergabe +wird nicht umgedeutet: Ein Regressionstest durch den realen Kodierbox-Adapter +und die echte 5.3.1-Engine erhält Status 13, Code/Score 0, alte Codeauswahl, +Alias und Subform. Keine Datenmigration und keine Status-Neuzuordnung. + +## Ergänzte Regressionen + +- 24 Bibliothekstests verwenden die echte Responses-Implementierung über + Validierung und SchemerService. Sie prüfen beide NO_VALUE-Schreibweisen, + sieben alte/neue Typbezeichnungen, elf alte/neue Formatbezeichnungen, + zwei Chemieformate und eine tatsächlich unzulässige STRING/BASE_NO_VALUE- + Kombination. Mit dem früheren privaten 5.2.2 scheiterten 22 dieser Tests; + mit 5.3.1 bestehen alle 24. Die anderen beiden Tests bestanden bereits zuvor. +- Drei Studio-Tests erzeugen den tatsächlich installierten öffentlichen + SchemerComponent, ohne Responses-Factory-Mock. NO_VALUE und no-value erhalten + Status OK; STRING/BASE_NO_VALUE bleibt ein blockierender INVALID_SOURCE-Fehler. + Der Großschreibungsfall scheiterte vor dem Paketwechsel. +- Vier Kodierbox-Tests prüfen die echte installierte Engine: beide NO_VALUE- + Schreibweisen, echte Quellenfehler und automatische Kodierung mit Code/Score 0. +- Der bestehende Kodierbox-Kompatibilitätsfilter berücksichtigt beide + NO_VALUE-Schreibweisen. Ein zusätzlicher Test verhindert, dass ein Fehler + für eine eigentlich passende uppercase-NO_VALUE-Quelle fälschlich toleriert wird. + Bestehende und ergänzte Tests prüfen den echten Validator in strict/compatible. + BASE/no-value wird vom aktuellen Validator nativ akzeptiert; BASE_NO_VALUE/STRING + bleibt tatsächlich unzulässig und wird nur nach der bestehenden ausdrücklich + gewählten Legacy-Kompatibilität toleriert. Strict lässt diesen Fehler blockierend. +- Ein zusätzlicher Service-Test prüft die oben beschriebene alte Codeauswahl + durch den echten aktuellen Autocoder einschließlich Alias-Routing. + +Die bewussten Validator-Korrekturen der 5.3-Reihe werden somit ausdrücklich +übernommen; die Validierung wird nicht pauschal abgeschaltet. Der Runtime-Code +für JSON-/DOCX-Erstellung wurde bei diesem Paketwechsel nicht geändert. + +## Lokale Prüfungen + +| Bereich | Ergebnis | +| --- | --- | +| coding-components Angular | 550 Tests bestanden | +| Generator, ESM und CommonJS | 1.285 Tests bestanden, keine übersprungen | +| Eigenständiger Schemer | 6 Tests und Build bestanden | +| Studio-Frontend, betroffener Bereich | 178 Tests in 9 Suites bestanden | +| Studio-API, betroffener Bereich | 145 Tests in 4 Suites bestanden | +| Kodierbox-Frontend, vollständige Suite | 2.102 Tests in 207 Suites bestanden | +| Kodierbox-Backend, vollständige Suite | 2.643 Tests bestanden, 17 bestehende Tests übersprungen | + +Bibliothek und alle vier Anwendungsbuilds bestanden. Frontend- und Backend- +Prüfungen liefen in getrennten, aufeinanderfolgenden Phasen. Host-Lint bestand; +Studio meldet bestehende Warnungen. Die neue Bibliotheks-Testdatei besteht ESLint. +Studios Docker-Versionsprüfung stimmt mit der frisch gebauten API überein; +Runtime-Lock, Root-Locks und Tarball-Prüfsummen sind abgeglichen. Eine neue +vollständige Docker-Runtime-Installation wurde für diesen Stand nicht ausgeführt. + +## Erneuter Installationsvergleich + +Beide Anwendungen enthalten dasselbe frisch gebaute und installierte 4.1.0-Artefakt: + +- SHA256: `9a0e75158dcc70e821c6a481d431ec6a0429e248afb85bd27991b1ae1af22d7a`. +- Alle 20 installierten Paketdateien entsprechen dem frischen Bibliotheksbuild. +- Beide Hosts und die Bibliothek lösen Responses auf denselben lokalen Pfad auf; + die installierte Version ist jeweils 5.3.1. Eine private Responses-Abhängigkeit + wird vom Vergleich explizit abgelehnt. +- Der installierte Validator akzeptiert uppercase NO_VALUE; die installierte + Regeltextdarstellung erkennt das neue Chemieformat. + +**4.694 Exportpaare bestehen:** 4.608 synthetische Matrixpaare, 6 ergänzende +Vertragsfälle, 40 Exporte gegen die 20 unabhängigen Studio-Leerzeichenreferenzen +und 40 Inlinebildfehler-Paare. Die Matrix umfasst alle 256 Kombinationen der acht +Ja/Nein-Optionen, drei Schulungsfilter, drei Missingkonstellationen und beide Formate. +Der Vergleich verwendet Studios DownloadWorkspacesClass und Kodierboxs echten +Generation-Service mit ihren Anwendungsadaptern. Datenquellen liegen im Speicher. +JSON wird strukturell verglichen, DOCX in allen normalisierten Paketbestandteilen: +Text, Tabellen, Styles, Fußzeilen, Seitenfelder, Medien, Beziehungen und Formeln. +Die neuen Versions-/Auflösungsprüfungen laufen vor diesen Exporten. + +Die 32 ursprünglichen Studio-Rohschema-Referenzen und 20 Leerzeichenreferenzen +wurden nicht neu erzeugt. Die Generator-Suite prüft sie weiterhin; gezielt +vereinbarte Normalisierungen wie die frühere 18pt-Korrektur bleiben unverändert. +Die Tests belegen die abgedeckten Fälle, keine allgemeine Fehlerfreiheit beliebiger +Daten. Die künstlichen Aufgaben sind weiterhin von den Beispiel-ZIPs unabhängig. + +Maschinenlesbarer aktueller Nachweis in coding-components: +`docs/codebook-responses-installation-comparison-2026-10-15.json`. + +## Grenzen und Einführung + +Für diesen Paketwechsel wurden kein neuer HTTP-/Browser-/Redis-Durchlauf und +kein neuer Microsoft-Word-Renderinglauf ausgeführt. Die frühere Word-Web-Abnahme +bezieht sich auf das damalige Artefakt. Desktop-Word unter Windows bleibt offen; +DOCX-XML-Gleichheit allein ist kein Nachweis identischer Windows-Word-Darstellung. +Die verpflichtende visuelle Abnahme und Prüfung beider realer Anwendungsabläufe +bleiben vor dem Ausrollen bestehen. + +Der eigenständige Schemer wurde mit 5.3.1 gebaut und getestet. Studios produktiv +über Verona geladene Schemer-Module erhalten diese Version erst durch ihre +separate Veröffentlichung und Einbindung; der Anwendungspaketwechsel ersetzt +keine bereits hochgeladenen Module. + +Dies ist eine lokale Prüfung des Release-Kandidaten. CI auf den gepushten +PR-Ständen ist ein separater Nachweis. Keine npm-Veröffentlichung, kein Merge, +kein Deployment und keine Produktionsabnahme wurden durchgeführt. diff --git a/docs/codebook-responses-installation-comparison-2026-10-15.json b/docs/codebook-responses-installation-comparison-2026-10-15.json new file mode 100644 index 0000000..1aa382a --- /dev/null +++ b/docs/codebook-responses-installation-comparison-2026-10-15.json @@ -0,0 +1,21 @@ +{ + "hostExportPairs": 4694, + "optionCombinations": 768, + "missingCases": 3, + "formats": 2, + "originalStudioWhitespaceCases": 20, + "inlineImageCases": 5, + "inlineImageExportPairs": 40, + "identicalJson": true, + "identicalAllDocxParts": true, + "artifactIntegrity": "sha512-PrPN0aAOvEBqcm6wAnJG//FIGKOU5Rzrf1Hwgot6neTUsS0IoS7wQ7ODOYXpTcYNWceSB+aq9Hmjg+rM5f7pKg==", + "identicalInstalledPackageFiles": true, + "installedPackageFiles": 20, + "responsesVersion": "5.3.1", + "responsesSharedWithHost": true, + "currentVariableInfoValidated": true, + "realHostAdapters": true, + "inMemoryRepositories": true, + "httpBrowserRedis": false, + "windowsWord": false +} \ No newline at end of file diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md index 1661d28..58bf012 100644 --- a/docs/shared-codebook.md +++ b/docs/shared-codebook.md @@ -1,8 +1,19 @@ # Gemeinsames Codebook: Integrationsvertrag -## Aktuelle Inlinebild-Korrektur +## Aktueller Responses-Stand -Die [Prüfung vom 15.10.2026](codebook-inline-images-2026-10-15.md) behebt Textverlust bei defekten Inlinebildern. 1.285 Generator-Tests und 4.694 installierte Exportpaare bestehen. Beide Anwendungen verwenden den Tarball mit SHA256 `1340db41e6493cca3d4395426c14f156319c3bbbf0b81d01f84c233a56a5091a`. Eine neue Word-Abnahme wurde für diesen Paketstand nicht ausgeführt. +Die [Prüfung vom 15.10.2026](codebook-responses-2026-10-15.md) verwendet überall +`@iqb/responses` 5.3.1, die beim Abgleich aktuelle Patchversion der 5.3-Reihe. +Die Bibliothek bezieht Responses als Peer (`^5.3.0`); Schemer, Validierung und +Generator verwenden dieselbe Installation wie der Host. Es gibt keine private +5.2.2-Installation mehr. 1.285 Generator-Tests und 4.694 installierte Exportpaare +bestehen einschließlich der unveränderten Studio-Referenzen. Aktueller Tarball- +SHA256: `9a0e75158dcc70e821c6a481d431ec6a0429e248afb85bd27991b1ae1af22d7a`. +Frühere Berichte unten beziehen sich auf ihre damaligen Paketstände. + +## Vorherige Inlinebild-Korrektur + +Die [Prüfung vom 15.10.2026](codebook-inline-images-2026-10-15.md) behebt Textverlust bei defekten Inlinebildern. 1.285 Generator-Tests und 4.694 installierte Exportpaare bestehen. Dabei verwendeten beide Anwendungen den Tarball mit SHA256 `1340db41e6493cca3d4395426c14f156319c3bbbf0b81d01f84c233a56a5091a`. Eine neue Word-Abnahme wurde für diesen Paketstand nicht ausgeführt. ## Format- und Aspect-Abnahme vom 13.10.2026 @@ -70,10 +81,13 @@ für Server. Der Generator braucht weder Angular noch DOM/Browser-Globals. Der CommonJS-Build integriert den ausschließlich als ESM gelieferten mathml2omml- Konverter. Dessen Quellcode bleibt über Source Maps und das npm-Original verfügbar. -Die Regeltextkonvertierung ist intern auf @iqb/responses 5.2.2 festgelegt. -Das ist ausdrücklich kein Upgrade der Kodierbox-Kodierengine: deren responses -5.2.0 und response-Spezifikation 1.5.1 bleiben unverändert. Die Bibliothek akzeptiert -response-Spezifikation 1.5.1 sowie 2.x als Peer. +Die Regeltextkonvertierung, Schemer und Validierung verwenden den vom Host +bereitgestellten Responses-Peer, aktuell gemeinsam geprüft mit 5.3.1. Kodierboxs +Kodierengine wird ebenfalls auf 5.3.1 aktualisiert. Ihr bestehender Anwendungs- +und Persistenzvertrag mit response-Spezifikation 1.5.1 bleibt erhalten; der interne +Engine-Vertrag stammt direkt aus Responses. Die Bibliothek akzeptiert +response-Spezifikation 1.5.1 sowie 2.x als Peer. Details und Regressionen stehen +im aktuellen Prüfbericht. ## Lokale Integration und Veröffentlichung diff --git a/scripts/compare-codebook-installations.mjs b/scripts/compare-codebook-installations.mjs index 6cc6bec..2c2a1ca 100644 --- a/scripts/compare-codebook-installations.mjs +++ b/scripts/compare-codebook-installations.mjs @@ -48,6 +48,8 @@ async function packageFiles(directory, prefix = '') { } const builtPackage = fileURLToPath(new URL('../dist/ngx-coding-components', import.meta.url)); const builtFiles = await packageFiles(builtPackage); +const testedLock = JSON.parse(await readFile(new URL('../package-lock.json', import.meta.url), 'utf8')); +const responsesVersion = testedLock.packages['node_modules/@iqb/responses'].version; const artifacts = []; for (const root of [studio, box]) { const lock = JSON.parse(await readFile(join(root, 'package-lock.json'), 'utf8')); @@ -61,6 +63,20 @@ for (const root of [studio, box]) { assert.deepEqual(await readFile(join(root, 'node_modules/@iqb/ngx-coding-components', file)), await readFile(join(builtPackage, file)), `stale or different installed package file: ${root}/${file}`); } + const hostRequire = createRequire(join(root, 'package.json')); + const libraryRequire = createRequire(join(root, 'node_modules/@iqb/ngx-coding-components/package.json')); + const manifest = libraryRequire('./package.json'); + assert.equal(manifest.dependencies['@iqb/responses'], undefined, 'Responses must not be privately pinned by the library'); + assert.ok(manifest.peerDependencies['@iqb/responses'], 'the host must supply the Responses peer'); + assert.equal(libraryRequire.resolve('@iqb/responses'), hostRequire.resolve('@iqb/responses'), + 'Schemer, validation and generator must resolve the same Responses implementation as their host'); + assert.equal(hostRequire('@iqb/responses/package.json').version, responsesVersion, 'host must use the tested version'); + const { CodingSchemeFactory, ToTextFactory } = libraryRequire('@iqb/responses'); + assert.deepEqual(CodingSchemeFactory.validate([{ id: 'V', type: 'NO_VALUE' }], + [{ id: 'V', sourceType: 'BASE_NO_VALUE', codes: [] }]), [], 'installed library must accept current NO_VALUE spelling'); + const variable = { id: 'V', type: 'STRING', format: 'CHEM_ELEMENT_SYMBOLS', + multiple: false, nullable: false, values: [], valuePositionLabels: [] }; + assert.match(ToTextFactory.varInfoAsText(variable).join(' '), /Chemische Elementsymbole/); } assert.equal(artifacts[0], artifacts[1], 'both installed artifacts must have the same integrity'); const Studio = (await hostModule(studio, 'apps/api/src/app/classes/download-workspaces.class.ts', { @@ -188,5 +204,6 @@ await writeFile(join(output, 'installation-comparison.json'), JSON.stringify({ h inlineImageCases: inlineImageCases.length, inlineImageExportPairs, identicalJson: true, identicalAllDocxParts: true, artifactIntegrity: artifacts[0], identicalInstalledPackageFiles: true, installedPackageFiles: builtFiles.length, + responsesVersion, responsesSharedWithHost: true, currentVariableInfoValidated: true, realHostAdapters: true, inMemoryRepositories: true, httpBrowserRedis: false, windowsWord: false }, null, 2)); console.log(`PASS: ${comparisons} export pairs through real Studio and Box preparation/adapters.`); From 1681cd898fe1fecdf174e015f63890fe4f1992fa Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:55:51 +0200 Subject: [PATCH 32/36] Require Responses 5.3.1 and cover stored chemistry formats --- projects/ngx-coding-components/package.json | 2 +- .../src/lib/services/responses-compatibility.spec.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/projects/ngx-coding-components/package.json b/projects/ngx-coding-components/package.json index 5cc1834..817b493 100644 --- a/projects/ngx-coding-components/package.json +++ b/projects/ngx-coding-components/package.json @@ -61,7 +61,7 @@ "prosemirror-state": "^1.3.4", "rxjs": "~7.8.0", "zone.js": "~0.15.0", - "@iqb/responses": "^5.3.0" + "@iqb/responses": "^5.3.1" }, "devDependencies": { "@angular-devkit/build-angular": "^20.0.4", diff --git a/projects/ngx-coding-components/src/lib/services/responses-compatibility.spec.ts b/projects/ngx-coding-components/src/lib/services/responses-compatibility.spec.ts index 5f046f7..8c78b0d 100644 --- a/projects/ngx-coding-components/src/lib/services/responses-compatibility.spec.ts +++ b/projects/ngx-coding-components/src/lib/services/responses-compatibility.spec.ts @@ -44,7 +44,9 @@ describe('Responses compatibility through real Schemer and validation imports', [ ['STRING', 'CHEM_ELEMENT_SYMBOLS', 'Chemische Elementsymbole'], - ['JSON', 'CHEM_MOLECULE', 'Molekül mit Bild'] + ['JSON', 'CHEM_MOLECULE', 'Molekül mit Bild'], + ['string', 'chem-element-symbols', 'Chemische Elementsymbole'], + ['json', 'chem-molecule', 'Molekül mit Bild'] ].forEach(([type, format, text]) => { it(`recognizes the new chemistry format ${format}`, () => { expect(ToTextFactory.varInfoAsText(playerVariable(type, format)).join(' ')).toContain(text); From 814209f7bd152b66c3519e2a34a0c3c9bf07c95d Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:55:51 +0200 Subject: [PATCH 33/36] Refresh installed codebook comparison and compatibility evidence --- ...odebook-solver-compatibility-2026-10-15.md | 112 ++++++++++++++++++ ...er-installation-comparison-2026-10-15.json | 23 ++++ docs/shared-codebook.md | 11 +- scripts/compare-codebook-installations.mjs | 12 +- 4 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 docs/codebook-solver-compatibility-2026-10-15.md create mode 100644 docs/codebook-solver-installation-comparison-2026-10-15.json diff --git a/docs/codebook-solver-compatibility-2026-10-15.md b/docs/codebook-solver-compatibility-2026-10-15.md new file mode 100644 index 0000000..af26e43 --- /dev/null +++ b/docs/codebook-solver-compatibility-2026-10-15.md @@ -0,0 +1,112 @@ +# SOLVER-Kompatibilität und Paket-Mindestversion vom 15.10.2026 + +Dieser Nachtrag ersetzt den Paketstand aus `codebook-responses-2026-10-15.md`. +Responses bleibt in allen Anwendungen und der Bibliotheksentwicklung bei 5.3.1. +Der öffentliche Peer der Bibliothek verlangt jetzt `^5.3.1`: 5.3.0 erkennt die +kleingeschriebenen Chemieformate gespeicherter Aufgaben noch nicht. +Zwei neue Bibliothekstests sichern zusätzlich `chem-molecule` und +`chem-element-symbols`; der Installationsvergleich prüft die Mindestversion und +beide Formen direkt über die tatsächlich installierten Pakete. + +## Erhaltenes Kodierverhalten der Kodierbox + +Das unveränderte npm-Responses 5.3.1 ändert gegenüber Kodierboxs bisherigem 5.2.0 +SOLVER-Ergebnisse: neue absolute mathjs-Toleranz und Kodierung von NULL-Ergebnissen. +Die Kodierbox erhält deshalb eine lokale, reproduzierbar erzeugte Engine-Fassung +**aus dem installierten Responses 5.3.1**, deren einziger umgeleiteter Import die +SOLVER-Mathematik ist. Der gesamte Ableitungsablauf bleibt aus 5.3.1; Codes und +abhängige Variablen sehen das richtige Ergebnis bereits vor der Codeauswahl. + +Der Adapter verwendet eine private Instanz der bisherigen mathjs-Version 12.4.3 +mit relativer Toleranz 1e-12 und schreibgeschützter Konfiguration. Dies erhält auch +weitere Math-Funktionen; nur die absolute Toleranz von mathjs 15 zu ändern wäre +keine vollständige Absicherung. NULL liefert wie bisher DERIVE_ERROR. +Auch verschachtelte evaluate/parser-Aufrufe können die Konfiguration nicht ändern. +Globale Math-Instanzen und die normale Responses-Factory werden nicht verändert. + +Validierung, Abhängigkeitsgraph, Schemer und Codebook-Regeltexte bleiben auf dem +normalen Responses-5.3.1-Peer. Studio behält seine bisherige eigene Engine-Semantik. +Die gemeinsame Bibliothek enthält keine Kodierbox-Kompatibilität und weiterhin +keine Jobsteuerung. Es gibt keine Datenmigration oder neue Statuszuordnung. + +## Regressionen und unabhängiger Vergleich + +Die drei neuen Service-Regressionen schlugen vor der Korrektur fehl und bestehen +nun über den echten Kodierbox-Namespace-/Aliasweg: + +| Fall | Bisheriges und nun wiederhergestelltes Ergebnis | +| --- | --- | +| ceil(A), A=1e-15, passende 0/1-Regeln | Wert, Code und Score 1 | +| A==0 ? 1:0, A=1e-15, passende 0/1-Regeln | Wert, Code und Score 0 | +| NULL-Zweig, auch mit IS_NULL-Code | DERIVE_ERROR; keine abgeschlossene Kodierung | + +Eine unabhängige Matrix vergleicht **11.760 Engine-Paare** mit dem unveränderten, +veröffentlichten Responses 5.2.0: sieben Ableitungsarten, 14 Statuswerte, 14 +Wertformen, Aliasvarianten, Subforms und neun SOLVER-Ausdrücke. Weitere Fälle +prüfen bestehende Zielzustände, BASE_NO_VALUE, SUM_SCORE-Ketten, nicht endliche +Ergebnisse, Konfigurationsschutz und ausbleibende globale Nebenwirkungen. +Das alte Referenzpaket ist ausschließlich eine Entwicklungsabhängigkeit und +wird nicht für Produktionskodierung verwendet. + +Der Builder `coding-box/scripts/build-autocoder-compat.mjs` bündelt die +installierten 5.3.1-Quellen, ohne sie manuell zu bearbeiten, und leitet genau einen +Math-Import um. Der Backend-Test baut das eingecheckte Artefakt erneut und +vergleicht es bytegenau; Version und Importanzahl sind geprüft. Details stehen +in `coding-box/docs/autocoder-solver-compatibility.md`. + +## Gemeinsames installiertes Paket + +Beide Hosts verwenden denselben neu gebauten 4.1.0-Tarball, SHA256: +`2df79a7b18196d7c25d7e02e15d5b148200d75ba2a315214ccfb8386536dd0a6`. +Root-Locks und Studios API-Runtime-Lock enthalten dessen passende Prüfsumme und +Peer-Mindestversion. Der Generator- und UI-Runtime-Code wurde dabei nicht geändert. + +Die unveränderten unabhängigen Studio-Referenzen bleiben verbindlich; sie werden +nicht aus dem neuen Generator neu erzeugt. Der erneute Installationsvergleich +prüft JSON strukturell und alle normalisierten DOCX-Bestandteile einschließlich +Bildern, Formeln, Styles, Tabellen, Beziehungen, Fußzeilen und Seitenfeldern. +Die echten Anwendungsadapter verwenden Datenquellen im Speicher. + +## Abschließende lokale Prüfung + +| Bereich | Ergebnis | +| --- | --- | +| Bibliotheks-Angular | 552 Tests bestanden | +| Eigenständiger Schemer | 6 Tests und Build bestanden | +| Generator ESM/CommonJS | 1.285 Tests bestanden, keine übersprungen | +| Studio-Frontend | 178 Tests in 9 betroffenen Suites bestanden | +| Studio-API | 145 Tests in 4 betroffenen Suites bestanden | +| Kodierbox-Frontend vollständig | 2.102 Tests bestanden | +| Kodierbox-Backend vollständig | 2.667 bestanden, 17 bestehende Tests übersprungen | + +Alle Hosttests liefen ohne Jest-/Nx-Cache. Bibliothek und alle vier Host-Builds +bestehen. Host-Lint und Lint der geänderten Bibliothekstestdatei bestehen; +Studio meldet bestehende Warnungen. Frontend- und Backend-Prüfungen wurden +getrennt und nacheinander ausgeführt. Die Studio-Testfixture verwendet einen +indexierten Statuszugriff, der auch bei frischer strikter Kompilierung gültig ist. + +**4.694 installierte Exportpaare bestehen erneut.** Vor dem Export werden alle +20 Paketdateien gegen den frischen Bibliotheksbuild geprüft. Die 32 unabhängigen +Studio-Rohschema- und 20 Abstandsreferenzen wurden nicht neu erzeugt und bestehen +weiterhin. Die bereits dokumentierten gezielten Formatkorrekturen bleiben erhalten. +Der aktuelle maschinenlesbare Nachweis liegt in coding-components unter +`docs/codebook-solver-installation-comparison-2026-10-15.json`; er enthält nun +zusätzlich Peer-Mindestversion und den Nachweis gespeicherter Chemieformate. + +Der gebaute Kodierbox-Backend-Code enthält die lokale SOLVER-Kompatibilität. +Sein Runtime-Manifest verlangt Responses 5.3.1 und mathjs 12.4.3 und enthält das +alte Referenzpaket nicht. Auch im Backend-Bundle kommt kein Referenzpaket-Import +vor. Studios Docker-Driftprüfung stimmt mit der frisch gebauten API überein; +Runtime-Lock und Tarball-Prüfsumme sind abgeglichen. Eine neue vollständige +Docker-Runtime-Installation wurde nicht durchgeführt. + + +## Nachweisgrenzen + +Ein aktueller HTTP-/Browser-/Redis-Lauf und ein neuer Microsoft-Renderinglauf +wurden für diese Korrektur nicht ausgeführt. Die frühere Word-Web-Abnahme gilt +für das damalige Artefakt; Windows-Desktop-Word bleibt offen. Die technische +XML-/Mediengleichheit ersetzt die visuelle Abnahme nicht. Bereits veröffentlichte +Verona-Schemer-Module benötigen weiterhin eine separate Aktualisierung. +Es wurden kein npm-Release, Merge, Deployment und keine Datenmigration ausgeführt. +Die CI der neuen PR-Heads ist ein separater Nachweis. diff --git a/docs/codebook-solver-installation-comparison-2026-10-15.json b/docs/codebook-solver-installation-comparison-2026-10-15.json new file mode 100644 index 0000000..b1a6bb5 --- /dev/null +++ b/docs/codebook-solver-installation-comparison-2026-10-15.json @@ -0,0 +1,23 @@ +{ + "hostExportPairs": 4694, + "optionCombinations": 768, + "missingCases": 3, + "formats": 2, + "originalStudioWhitespaceCases": 20, + "inlineImageCases": 5, + "inlineImageExportPairs": 40, + "identicalJson": true, + "identicalAllDocxParts": true, + "artifactIntegrity": "sha512-D3xPeMziVpNqjTo662iG31mw9BWrdgTthdL2BtyASeCSNohR+B3s4C5IvMwNBa8P34txkG5/EEir9qN0AqhQGQ==", + "identicalInstalledPackageFiles": true, + "installedPackageFiles": 20, + "responsesVersion": "5.3.1", + "responsesPeerRange": "^5.3.1", + "responsesSharedWithHost": true, + "currentVariableInfoValidated": true, + "storedChemistryFormatsRecognized": true, + "realHostAdapters": true, + "inMemoryRepositories": true, + "httpBrowserRedis": false, + "windowsWord": false +} \ No newline at end of file diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md index 58bf012..e40fc35 100644 --- a/docs/shared-codebook.md +++ b/docs/shared-codebook.md @@ -1,6 +1,15 @@ # Gemeinsames Codebook: Integrationsvertrag -## Aktueller Responses-Stand +## Aktuelle SOLVER-Kompatibilität und Mindestversion + +Der [Nachtrag vom 15.10.2026](codebook-solver-compatibility-2026-10-15.md) erhält +Kodierboxs bisherige SOLVER-Ergebnisse mit Engine-Code aus Responses 5.3.1 und +isolierter bisheriger Mathematik. Die Bibliothek verlangt nun den Peer `^5.3.1` +und prüft Chemieformate auch in gespeicherter Kleinschreibung. Aktueller Tarball- +SHA256: `2df79a7b18196d7c25d7e02e15d5b148200d75ba2a315214ccfb8386536dd0a6`. +Frühere Abschnitte und Berichte unten beziehen sich auf damalige Paketstände. + +## Vorheriger Responses-Abgleich Die [Prüfung vom 15.10.2026](codebook-responses-2026-10-15.md) verwendet überall `@iqb/responses` 5.3.1, die beim Abgleich aktuelle Patchversion der 5.3-Reihe. diff --git a/scripts/compare-codebook-installations.mjs b/scripts/compare-codebook-installations.mjs index 2c2a1ca..3e56fd7 100644 --- a/scripts/compare-codebook-installations.mjs +++ b/scripts/compare-codebook-installations.mjs @@ -67,7 +67,7 @@ for (const root of [studio, box]) { const libraryRequire = createRequire(join(root, 'node_modules/@iqb/ngx-coding-components/package.json')); const manifest = libraryRequire('./package.json'); assert.equal(manifest.dependencies['@iqb/responses'], undefined, 'Responses must not be privately pinned by the library'); - assert.ok(manifest.peerDependencies['@iqb/responses'], 'the host must supply the Responses peer'); + assert.equal(manifest.peerDependencies['@iqb/responses'], '^5.3.1', 'the peer must exclude 5.3.0'); assert.equal(libraryRequire.resolve('@iqb/responses'), hostRequire.resolve('@iqb/responses'), 'Schemer, validation and generator must resolve the same Responses implementation as their host'); assert.equal(hostRequire('@iqb/responses/package.json').version, responsesVersion, 'host must use the tested version'); @@ -77,6 +77,13 @@ for (const root of [studio, box]) { const variable = { id: 'V', type: 'STRING', format: 'CHEM_ELEMENT_SYMBOLS', multiple: false, nullable: false, values: [], valuePositionLabels: [] }; assert.match(ToTextFactory.varInfoAsText(variable).join(' '), /Chemische Elementsymbole/); + for (const [type, format, expected] of [ + ['string', 'chem-element-symbols', /Chemische Elementsymbole/], + ['json', 'chem-molecule', /Molekül mit Bild/] + ]) { + assert.match(ToTextFactory.varInfoAsText({ ...variable, type, format }).join(' '), expected, + 'installed Responses must recognize chemistry formats from stored unit XML'); + } } assert.equal(artifacts[0], artifacts[1], 'both installed artifacts must have the same integrity'); const Studio = (await hostModule(studio, 'apps/api/src/app/classes/download-workspaces.class.ts', { @@ -204,6 +211,7 @@ await writeFile(join(output, 'installation-comparison.json'), JSON.stringify({ h inlineImageCases: inlineImageCases.length, inlineImageExportPairs, identicalJson: true, identicalAllDocxParts: true, artifactIntegrity: artifacts[0], identicalInstalledPackageFiles: true, installedPackageFiles: builtFiles.length, - responsesVersion, responsesSharedWithHost: true, currentVariableInfoValidated: true, + responsesVersion, responsesPeerRange: '^5.3.1', responsesSharedWithHost: true, + currentVariableInfoValidated: true, storedChemistryFormatsRecognized: true, realHostAdapters: true, inMemoryRepositories: true, httpBrowserRedis: false, windowsWord: false }, null, 2)); console.log(`PASS: ${comparisons} export pairs through real Studio and Box preparation/adapters.`); From 14432a346e75acdaabfd3394cdacb07b4dd65f29 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Thu, 15 Oct 2026 13:30:29 +0200 Subject: [PATCH 34/36] Document codebook verification after host integration sync --- ...odebook-solver-compatibility-2026-10-15.md | 33 +++++++++++++++++-- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/docs/codebook-solver-compatibility-2026-10-15.md b/docs/codebook-solver-compatibility-2026-10-15.md index af26e43..d1133f6 100644 --- a/docs/codebook-solver-compatibility-2026-10-15.md +++ b/docs/codebook-solver-compatibility-2026-10-15.md @@ -76,8 +76,10 @@ Die echten Anwendungsadapter verwenden Datenquellen im Speicher. | Generator ESM/CommonJS | 1.285 Tests bestanden, keine übersprungen | | Studio-Frontend | 178 Tests in 9 betroffenen Suites bestanden | | Studio-API | 145 Tests in 4 betroffenen Suites bestanden | -| Kodierbox-Frontend vollständig | 2.102 Tests bestanden | -| Kodierbox-Backend vollständig | 2.667 bestanden, 17 bestehende Tests übersprungen | +| Kodierbox-Frontend vollständig | 2.516 Tests bestanden | +| Kodierbox-Zoneless | 750 Tests bestanden | +| Kodierbox-Paketkorrekturen | 19 Tests bestanden | +| Kodierbox-Backend vollständig | 2.682 bestanden, 17 bestehende Tests übersprungen | Alle Hosttests liefen ohne Jest-/Nx-Cache. Bibliothek und alle vier Host-Builds bestehen. Host-Lint und Lint der geänderten Bibliothekstestdatei bestehen; @@ -101,6 +103,31 @@ Runtime-Lock und Tarball-Prüfsumme sind abgeglichen. Eine neue vollständige Docker-Runtime-Installation wurde nicht durchgeführt. +## Erneute Prüfung nach Kodierbox-develop-Abgleich + +Die Kodierbox wurde mit `develop` auf `ab8d6f250` (Angular-Zoneless-Grundlage, +PR #1048) zusammengeführt. Die vier Konflikte betrafen die beiden CI-Dateien, +`package.json` und `package-lock.json`. Die eingegangenen Sicherheitsupdates, +Paketkorrekturen und Zoneless-Änderungen bleiben erhalten; Responses 5.3.1, +der gemeinsame Tarball, mathjs 12.4.3 und die reine Entwicklungsreferenz wurden +beibehalten. Das vollständige Node-22-Bookworm-Image und die getrennten +Frontend-/Backend-Prüfungen bleiben in der CI erhalten. + +Das aktualisierte esbuild 0.28.1 erfordert eine neue Erzeugung der eingecheckten +Kompatibilitätsengine. Die Änderung betrifft dessen Modulinitialisierungshelfer; +der bytegenaue Reproduzierbarkeitstest und die unabhängigen Engine-Vergleiche +bestehen mit dem neuen Artefakt. Der Codebook-Zoneless-Test verwendet jetzt +`whenStable()` und Jest-Timer anstelle des Zone.js-abhängigen `fakeAsync`. +Seine drei Fälle bestehen auch im normalen Frontend-Testlauf. Sie prüfen weiterhin +asynchrones Laden, sichtbaren Fortschritt, Downloadsperre und Polling-Abbruch. + +Die Tabelle oben enthält die nach diesem Abgleich wiederholten Kodierbox-Prüfungen. +Die 19 Paketkorrekturtests sichern außerdem die aus `develop` übernommenen +Abhängigkeiten ab. Der Installationsvergleich wurde anschließend mit den +unveränderten installierten Studio-/Bibliothekspaketen erneut ausgeführt; +Bibliotheks- und Studio-Builds/Tests wurden durch diesen Kodierbox-Abgleich nicht +verändert. Die unabhängigen Studio-Referenzdateien bleiben unverändert. + ## Nachweisgrenzen Ein aktueller HTTP-/Browser-/Redis-Lauf und ein neuer Microsoft-Renderinglauf @@ -108,5 +135,5 @@ wurden für diese Korrektur nicht ausgeführt. Die frühere Word-Web-Abnahme gil für das damalige Artefakt; Windows-Desktop-Word bleibt offen. Die technische XML-/Mediengleichheit ersetzt die visuelle Abnahme nicht. Bereits veröffentlichte Verona-Schemer-Module benötigen weiterhin eine separate Aktualisierung. -Es wurden kein npm-Release, Merge, Deployment und keine Datenmigration ausgeführt. +Es wurden kein npm-Release, kein PR-Merge, kein Deployment und keine Datenmigration ausgeführt. Die CI der neuen PR-Heads ist ein separater Nachweis. From bf6f1ae554e72e845a2a9fb2c2f64645413ffca4 Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:46:55 +0200 Subject: [PATCH 35/36] Fix Windows Word acceptance runner and PDF verification --- scripts/verify-codebook-word-pdfs.py | 78 ++++++++ scripts/verify-codebook-word-windows.ps1 | 222 +++++++++++++++++------ 2 files changed, 243 insertions(+), 57 deletions(-) create mode 100644 scripts/verify-codebook-word-pdfs.py diff --git a/scripts/verify-codebook-word-pdfs.py b/scripts/verify-codebook-word-pdfs.py new file mode 100644 index 0000000..50d701b --- /dev/null +++ b/scripts/verify-codebook-word-pdfs.py @@ -0,0 +1,78 @@ +# Reproduce native Word PDF checks after verify-codebook-word-windows.ps1. +# Requires Python 3, Pillow, pypdf and Poppler pdftoppm. Does not assert visual acceptance. +import argparse +import shutil +from pathlib import Path +import hashlib +import json +import re +import subprocess +from collections import defaultdict +from PIL import Image, ImageChops +from pypdf import PdfReader + +parser = argparse.ArgumentParser(description='Verify and raster-compare native Windows Word PDFs.') +parser.add_argument('directory', type=Path, help='WordResults folder containing the completed JSON and PDFs') +parser.add_argument('--pdftoppm', default=shutil.which('pdftoppm'), help='Path to Poppler pdftoppm') +args = parser.parse_args() +if not args.pdftoppm: + parser.error('pdftoppm is required; add it to PATH or pass --pdftoppm') +root = args.directory +if not root.is_dir(): + parser.error('The completed WordResults directory does not exist') +evidence = root +report = json.loads((evidence / 'word-windows-result.json').read_text(encoding='utf-8-sig')) +assert report['automatedChecksPassed'], report['failure'] +assert report['inputHashesUnchanged'] +renders = root / 'renders' +renders.mkdir(exist_ok=True) +groups = defaultdict(list) +documents = [] +for item in report['documents']: + pdf = evidence / item['pdf'] + reader = PdfReader(pdf) + assert len(reader.pages) == item['pages'] + assert 'Word' in reader.metadata.producer, reader.metadata + stem = pdf.stem + prefix = renders / stem + subprocess.run([ + args.pdftoppm, + '-r', '120', '-png', str(pdf), str(prefix) + ], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + pages = [] + for number, page in enumerate(reader.pages, 1): + text = page.extract_text() + match = re.search(r'Seite\s+(\d+)\s+von\s+(\d+)', text) + assert match and tuple(map(int, match.groups())) == (number, len(reader.pages)), (stem, number, text) + image = renders / f'{stem}-{number}.png' + assert image.exists() + pages.append({'page': number, 'png': image.name, + 'pixelSha256': hashlib.sha256(Image.open(image).tobytes()).hexdigest()}) + record = {k: item[k] for k in ('file', 'alias', 'kind', 'host', 'case', 'pages', 'tables', 'images', 'formulas')} + record.update(pdfSha256=hashlib.sha256(pdf.read_bytes()).hexdigest(), + creator=reader.metadata.creator, producer=reader.metadata.producer, + renderedPages=pages) + documents.append(record) + if item['kind'] == 'current-installed-host-export': + groups[item['case']].append(record) + +pairs = [] +for case, pair in groups.items(): + assert len(pair) == 2 + assert pair[0]['pages'] == pair[1]['pages'] + for left, right in zip(pair[0]['renderedPages'], pair[1]['renderedPages']): + a = Image.open(renders / left['png']).convert('RGB') + b = Image.open(renders / right['png']).convert('RGB') + assert a.size == b.size + diff = ImageChops.difference(a, b) + assert diff.getbbox() is None, (case, left['page'], diff.getbbox()) + pairs.append({'case': case, 'files': [d['file'] for d in pair], 'pagesPerHost': pair[0]['pages'], + 'identicalRasterPixels': True}) + +result = {'platform': report['platform'], 'automatedChecksPassed': True, + 'totalDocuments': len(documents), 'totalPages': sum(d['pages'] for d in documents), + 'currentHostPairs': len(pairs), 'inputHashesUnchanged': True, + 'allPageNumbersVerifiedInWordPdfs': True, 'allCurrentHostPixelsIdentical': True, + 'visualInspectionComplete': False, 'documents': documents, 'pairs': pairs} +(root / 'word-pdf-comparison.json').write_text(json.dumps(result, indent=2) + '\n') +print(json.dumps({k: v for k, v in result.items() if k not in ('documents', 'pairs')}, indent=2)) diff --git a/scripts/verify-codebook-word-windows.ps1 b/scripts/verify-codebook-word-windows.ps1 index 3566144..ef00178 100644 --- a/scripts/verify-codebook-word-windows.ps1 +++ b/scripts/verify-codebook-word-windows.ps1 @@ -1,95 +1,203 @@ param( - [Parameter(Mandatory = $true)][string]$Directory + [Parameter(Mandatory = $true)][string]$Directory, + [switch]$IncludeStudioReferences ) -# Run on Windows with desktop Microsoft Word installed. Opens copies read-only; -# never saves changes to the DOCX sources. PDF and JSON are acceptance evidence, -# not a replacement for inspecting the rendered pages. +# Windows desktop Word acceptance for the staged synthetic codebook documents. +# Sources are opened read-only, without OpenAndRepair, and never saved. $ErrorActionPreference = 'Stop' -if ($env:OS -ne 'Windows_NT') { throw 'This check requires Microsoft Word on Windows.' } -$folder = (Resolve-Path $Directory).Path -$manifest = Get-Content (Join-Path $folder 'word-expectations.json') -Raw | ConvertFrom-Json -if (@($manifest.cases).Count -ne 3) { throw 'Expected three Word acceptance cases.' } -$suffixes = @($manifest.cases | ForEach-Object { $_.suffix } | Sort-Object -Unique) -if (($suffixes -join ',') -ne ',-rich-no-scores,-rich-scores') { throw 'Unexpected Word acceptance cases.' } -$word = $null +if ($env:OS -ne 'Windows_NT') { throw 'Windows desktop Word is required.' } +$folder = (Resolve-Path -LiteralPath $Directory).Path +$expectations = Get-Content (Join-Path $folder 'word-expectations.json') -Raw | ConvertFrom-Json +# Accept both the original three-case matrix and the seven-case inline-image matrix. +$baseline = @('', '-rich-no-scores', '-rich-scores') +$inline = @('-inline-mixed-code-no-scores', '-inline-mixed-code-scores', + '-inline-mixed-general-no-scores', '-inline-mixed-general-scores') +$suffixes = @($expectations.cases | ForEach-Object { $_.suffix }) +$accepted = if ($suffixes.Count -eq 3) { $baseline } else { $baseline + $inline } +if ($suffixes.Count -ne $accepted.Count -or + (Compare-Object ($suffixes | Sort-Object) ($accepted | Sort-Object))) { + throw 'Expected the complete three- or seven-case Word acceptance matrix.' +} +$files = @() +foreach ($expected in $expectations.cases) { + foreach ($hostName in @('studio', 'kodierbox')) { + $name = "$hostName$($expected.suffix).docx" + $files += [pscustomobject]@{ + file = $name; uiAlias = ('W{0:D2}.docx' -f ($files.Count + 1)) + sha256 = (Get-FileHash -LiteralPath (Join-Path $folder $name) -Algorithm SHA256).Hash + kind = 'current-installed-host-export'; host = $hostName + case = $expected.suffix; expected = $expected + } + } +} +if ($IncludeStudioReferences) { + foreach ($scoreMode in @('scores', 'no-scores')) { + $name = "original-studio-$scoreMode.docx" + $expected = @($expectations.cases | Where-Object { $_.suffix -eq "-rich-$scoreMode" })[0] + $files += [pscustomobject]@{ + file = $name; uiAlias = ('W{0:D2}.docx' -f ($files.Count + 1)) + sha256 = (Get-FileHash -LiteralPath (Join-Path $folder $name) -Algorithm SHA256).Hash + kind = 'independent-pre-harmonization-studio-reference'; host = 'original-studio' + case = $expected.suffix; expected = $expected + } + } +} +$manifest = [pscustomobject]@{ files = $files } +$output = Join-Path $folder ('WordResults' + (Get-Date -Format 'yyyyMMddHHmmss') + '-' + [guid]::NewGuid().ToString('N').Substring(0, 6)) +New-Item -ItemType Directory -Path $output | Out-Null +function Write-Stage([string]$message) { + Add-Content -Path $progressFile -Value ((Get-Date -Format o) + ' ' + $message) -Encoding UTF8 +} +$localInput = Join-Path $env:TEMP ('CodebookWord' + [guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Path $localInput | Out-Null +$progressFile = Join-Path $localInput 'progress.log' +Write-Stage "Starting process $PID" $results = @() +$word = $null +$ownInstance = $false +$failure = $null try { $word = New-Object -ComObject Word.Application - $word.Visible = $false + Write-Stage 'Created isolated Word instance' + # Refuse to operate on a COM instance containing the user's open documents. + if ($word.Documents.Count -ne 0) { throw 'Word instance already contains open documents; stopping.' } + $ownInstance = $true + $word.Visible = $true $word.DisplayAlerts = 0 $word.AutomationSecurity = 3 - foreach ($expected in $manifest.cases) { - foreach ($hostName in @('studio', 'kodierbox')) { - $name = "$hostName$($expected.suffix)" + foreach ($file in $manifest.files) { $document = $null try { - $path = Join-Path $folder "$name.docx" + [string]$source = Join-Path $folder $file.file + if ((Get-FileHash $source -Algorithm SHA256).Hash -ne $file.sha256) { + throw "Input checksum mismatch: $($file.uiAlias)" + } + [string]$path = Join-Path $localInput $file.uiAlias + Copy-Item -LiteralPath $source -Destination $path + Write-Stage "Opening $($file.uiAlias) from verified local copy" # ConfirmConversions=false, ReadOnly=true, AddToRecentFiles=false. - # Open normally; do not request Word's OpenAndRepair mode. - $document = $word.Documents.Open($path, $false, $true, $false) + # OpenAndRepair remains its default false; no repair fallback is used. + $confirmConversions = $false + $readOnly = $true + $addToRecentFiles = $false + $document = $word.Documents.Open([ref]$path, [ref]$confirmConversions, [ref]$readOnly, [ref]$addToRecentFiles) + Write-Stage "Opened $($file.uiAlias)" $document.Repaginate() - if ($document.Fields.Update() -ne 0) { throw "$name could not update all document fields." } + Write-Stage 'Repaginated' + if ($document.Fields.Update() -ne 0) { throw 'Failed to update document fields.' } foreach ($section in $document.Sections) { foreach ($footer in $section.Footers) { - if ($footer.Range.Fields.Update() -ne 0) { throw "$name could not update all footer fields." } + if ($footer.Range.Fields.Update() -ne 0) { throw 'Failed to update footer fields.' } } } $document.Repaginate() + Write-Stage 'Updated fields' $pages = $document.ComputeStatistics(2) $tables = $document.Tables.Count $images = $document.InlineShapes.Count $formulas = $document.OMaths.Count $headings = @($document.Paragraphs | Where-Object { $_.OutlineLevel -eq 2 }).Count - if ($pages -lt 2) { throw "$name must exercise multiple pages." } - if ($tables -ne $expected.tables) { throw "$name lost tables: $tables / $($expected.tables)" } - if ($images -ne $expected.images) { throw "$name lost images: $images / $($expected.images)" } - if ($formulas -ne $expected.formulas) { throw "$name lost equations: $formulas / $($expected.formulas)" } - if ($headings -ne $expected.variableHeadings) { throw "$name lost variable headings." } - foreach ($table in $document.Tables) { - if ($table.Columns.Count -ne $expected.columns) { throw "$name has an incorrect table column count." } - if ($table.Rows.AllowBreakAcrossPages -ne 0) { throw "$name allows code rows to split across pages." } - } + $text = $document.Content.Text $footerText = $document.Sections.Item(1).Footers.Item(1).Range.Text - if (-not $footerText.Contains('IQB Codebook')) { throw "$name is missing the footer label." } + Write-Stage "Collected counts: pages $pages tables $tables images $images formulas $formulas" + $expected = $file.expected + if ($tables -ne $expected.tables) { throw "Lost tables: $tables / $($expected.tables)" } + if ($images -ne $expected.images) { throw "Lost images: $images / $($expected.images)" } + if ($formulas -ne $expected.formulas) { throw "Lost equations: $formulas / $($expected.formulas)" } + if ($headings -ne $expected.variableHeadings) { throw 'Lost variable headings.' } + if (-not $file.case.Contains('-inline-') -and $pages -lt 2) { throw 'Multi-page coverage missing.' } + Write-Stage 'Validating tables' + for ($tableIndex = 1; $tableIndex -le $document.Tables.Count; $tableIndex++) { + $table = $document.Tables.Item($tableIndex) + if ($table.Columns.Count -ne $expected.columns) { throw 'Incorrect table column count.' } + if ($table.Rows.AllowBreakAcrossPages -ne 0) { throw 'Code rows can split across pages.' } + } + Write-Stage 'Validated tables' + $footerLabel = if ($file.host -eq 'original-studio') { 'IQB-Studio Codebook' } else { 'IQB Codebook' } + if (-not $footerText.Contains($footerLabel)) { throw 'Footer label missing.' } $footerFields = $document.Sections.Item(1).Footers.Item(1).Range.Fields - $pageFields = @($footerFields | Where-Object { $_.Type -eq 33 }) # wdFieldPage - $totalFields = @($footerFields | Where-Object { $_.Type -eq 26 }) # wdFieldNumPages - if ($pageFields.Count -ne 1 -or $totalFields.Count -ne 1) { throw "$name lost PAGE or NUMPAGES." } - if ([int]$totalFields[0].Result.Text.Trim() -ne $pages) { throw "$name has an incorrect total page field." } + Write-Stage "Validating $($footerFields.Count) footer fields" + $pageFields = @() + $totalFields = @() + for ($fieldIndex = 1; $fieldIndex -le $footerFields.Count; $fieldIndex++) { + $field = $footerFields.Item($fieldIndex) + if ($field.Type -eq 33) { $pageFields += $field } + if ($field.Type -eq 26) { $totalFields += $field } + } + if ($pageFields.Count -ne 1 -or $totalFields.Count -ne 1) { throw 'PAGE / NUMPAGES missing.' } + if ([int]$totalFields[0].Result.Text.Trim() -ne $pages) { throw 'Incorrect total page field.' } $pageValue = [int]$pageFields[0].Result.Text.Trim() - if ($pageValue -lt 1 -or $pageValue -gt $pages) { throw "$name has an incorrect current page field." } - $text = $document.Content.Text - if ($expected.hasMissingZero -and -not $text.Contains('0 Missing zero')) { throw "$name is missing Missing 0." } - $document.ExportAsFixedFormat((Join-Path $folder "$name-word-windows.pdf"), 17) + if ($pageValue -lt 1 -or $pageValue -gt $pages) { throw 'Incorrect current page field.' } + Write-Stage 'Validated page fields' + if ($expected.hasMissingZero -and -not $text.Contains('0 Missing zero')) { throw 'Missing 0 lost.' } + if ($file.case.Contains('-inline-')) { + foreach ($token in @('VOR', 'NACH', 'WEITER', 'HTML konnte nicht verarbeitet werden.')) { + if (-not $text.Contains($token)) { throw "Inline-image surrounding content lost: $token" } + } + } + $pdf = [IO.Path]::GetFileNameWithoutExtension($file.uiAlias) + '.pdf' + [string]$pdfPath = Join-Path $output $pdf + Write-Stage "Exporting $pdf" + $document.ExportAsFixedFormat($pdfPath, 17) + Write-Stage "Exported $pdf" $results += [pscustomobject]@{ - case = $expected.suffix; file = "$name.docx"; pages = $pages; tables = $tables; images = $images; formulas = $formulas - text = $text; footer = $footerText; wordVersion = $word.Version; wordBuild = $word.Build + file = $file.file; alias = $file.uiAlias; kind = $file.kind; host = $file.host + case = $file.case; pages = $pages; tables = $tables; images = $images + formulas = $formulas; variableHeadings = $headings; text = $text; footer = $footerText + pdf = $pdf; wordVersion = $word.Version; wordBuild = $word.Build } + Write-Host "PASS $($file.uiAlias) $pages pages $tables tables $images images $formulas equations" } finally { if ($null -ne $document) { - $document.Close(0) + $saveChanges = 0 + $document.Close([ref]$saveChanges) + Write-Stage "Closed $($file.uiAlias) without saving" [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($document) } } - } - } - foreach ($group in @($results | Group-Object -Property case)) { - $pair = $group.Group - if ($pair.Count -ne 2) { throw 'Missing host document in Word comparison.' } - if ($pair[0].text -cne $pair[1].text) { throw "$($group.Name): different content in Word." } - if ($pair[0].pages -ne $pair[1].pages) { throw "$($group.Name): different page counts in Word." } - if ($pair[0].footer -cne $pair[1].footer) { throw "$($group.Name): different footers in Word." } } - $report = [pscustomobject]@{ - checkedAtUtc = [DateTime]::UtcNow.ToString('o'); platform = 'Windows desktop Word' - automatedChecksPassed = $true; visualAcceptanceComplete = $false - documents = @($results | Select-Object case,file,pages,tables,images,formulas,wordVersion,wordBuild) + foreach ($group in @($results | Where-Object { $_.kind -eq 'current-installed-host-export' } | Group-Object -Property case)) { + $pair = @($group.Group) + if ($pair.Count -ne 2) { throw 'Host pair missing.' } + if ($pair[0].text -cne $pair[1].text) { throw "Different host text: $($group.Name)" } + if ($pair[0].pages -ne $pair[1].pages) { throw "Different host page counts: $($group.Name)" } + if ($pair[0].footer -cne $pair[1].footer) { throw "Different host footers: $($group.Name)" } } - $report | ConvertTo-Json -Depth 5 | Set-Content (Join-Path $folder 'word-windows-result.json') -Encoding UTF8 - Write-Output 'Word checks passed. Inspect all six PDFs for page breaks, fonts, formulas, images and page fields before visual acceptance.' +} catch { + $failure = $_.Exception.ToString() + Write-Host $failure -ForegroundColor Red } finally { if ($null -ne $word) { - $word.Quit(0) - [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($word) + try { + if ($ownInstance) { + $saveChanges = 0 + $word.Quit([ref]$saveChanges) + } + } catch { + $failure = "$failure`nWord cleanup: $($_.Exception)" + } finally { + [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($word) + } + } + $hashesUnchanged = $true + foreach ($file in $manifest.files) { + if ((Get-FileHash (Join-Path $folder $file.file) -Algorithm SHA256).Hash -ne $file.sha256) { + $hashesUnchanged = $false + } + } + if (-not $hashesUnchanged) { $failure = "$failure`nSource checksums changed." } + $report = [pscustomobject]@{ + checkedAtUtc = [DateTime]::UtcNow.ToString('o'); platform = 'Windows desktop Word' + automatedChecksPassed = ($null -eq $failure -and $results.Count -eq $manifest.files.Count -and $hashesUnchanged) + visualAcceptanceComplete = $false; inputHashesUnchanged = $hashesUnchanged + failure = $failure; documents = $results; inputs = $manifest } + Copy-Item -LiteralPath $progressFile -Destination (Join-Path $output 'progress.log') + $pendingReport = Join-Path $output 'word-windows-result.pending.json' + $report | ConvertTo-Json -Depth 12 | Set-Content $pendingReport -Encoding UTF8 + Move-Item -LiteralPath $pendingReport -Destination (Join-Path $output 'word-windows-result.json') + Write-Host "Evidence: $output" + Remove-Item -LiteralPath $localInput -Recurse -Force } +if ($null -ne $failure) { throw $failure } +Write-Host 'Automated checks complete. Visually inspect all Word PDFs before acceptance.' From 836e5d99e77250620e07678e8842050ded3e335c Mon Sep 17 00:00:00 2001 From: jurei733 <67505990+jurei733@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:46:59 +0200 Subject: [PATCH 36/36] Record native Windows Word codebook acceptance --- ...odebook-solver-compatibility-2026-10-15.md | 16 +- docs/codebook-word-windows-2026-10-15.json | 601 ++++++++++++++++++ docs/codebook-word-windows-2026-10-15.md | 118 ++++ docs/shared-codebook.md | 18 +- 4 files changed, 745 insertions(+), 8 deletions(-) create mode 100644 docs/codebook-word-windows-2026-10-15.json create mode 100644 docs/codebook-word-windows-2026-10-15.md diff --git a/docs/codebook-solver-compatibility-2026-10-15.md b/docs/codebook-solver-compatibility-2026-10-15.md index d1133f6..0e7a014 100644 --- a/docs/codebook-solver-compatibility-2026-10-15.md +++ b/docs/codebook-solver-compatibility-2026-10-15.md @@ -130,10 +130,16 @@ verändert. Die unabhängigen Studio-Referenzdateien bleiben unverändert. ## Nachweisgrenzen -Ein aktueller HTTP-/Browser-/Redis-Lauf und ein neuer Microsoft-Renderinglauf -wurden für diese Korrektur nicht ausgeführt. Die frühere Word-Web-Abnahme gilt -für das damalige Artefakt; Windows-Desktop-Word bleibt offen. Die technische -XML-/Mediengleichheit ersetzt die visuelle Abnahme nicht. Bereits veröffentlichte -Verona-Schemer-Module benötigen weiterhin eine separate Aktualisierung. +Die ergänzte [Windows-Desktop-Word-Abnahme](codebook-word-windows-2026-10-15.md) +prüft den oben bezeichneten aktuellen Tarball: 16 synthetische Dokumente, +32 vollständig gesichtete Seiten, sieben pixelgleiche aktuelle Host-Paare und +zwei unveränderte alte Studio-Referenzen. Word LTSC 2024, Version 2408, +Build 16.0.17932.21000 (64 Bit), besteht auch den Wiederholungslauf der endgültigen +Prüfskriptfassung. Die vereinbarte Kennzeichnung und die dokumentierte +18pt-Korrektur bleiben die sichtbaren Unterschiede zur alten Rich-Text-Referenz. + +Ein aktueller HTTP-/Browser-/Redis-Lauf wurde für diese Korrektur nicht erneut +ausgeführt; die Word-Abnahme ersetzt diese Anwendungsprüfungen nicht. +Bereits veröffentlichte Verona-Schemer-Module benötigen weiterhin eine separate Aktualisierung. Es wurden kein npm-Release, kein PR-Merge, kein Deployment und keine Datenmigration ausgeführt. Die CI der neuen PR-Heads ist ein separater Nachweis. diff --git a/docs/codebook-word-windows-2026-10-15.json b/docs/codebook-word-windows-2026-10-15.json new file mode 100644 index 0000000..c09187f --- /dev/null +++ b/docs/codebook-word-windows-2026-10-15.json @@ -0,0 +1,601 @@ +{ + "platform": "Windows desktop Word", + "automatedChecksPassed": true, + "totalDocuments": 16, + "totalPages": 32, + "currentHostPairs": 7, + "inputHashesUnchanged": true, + "allPageNumbersVerifiedInWordPdfs": true, + "allCurrentHostPixelsIdentical": true, + "visualInspectionComplete": true, + "documents": [ + { + "file": "kodierbox-inline-mixed-code-no-scores.docx", + "alias": "W01R1.docx", + "kind": "current-installed-host-export", + "host": "kodierbox", + "case": "-inline-mixed-code-no-scores", + "pages": 1, + "tables": 1, + "images": 1, + "formulas": 1, + "pdfSha256": "141f7055351a656acbd4a12d9986c57827ec725387c5f41b9dc18a4036a9f8e2", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W01R1-1.png", + "pixelSha256": "4e6105af72140d0033d546706a6e337aaa6def96982bd7ed09feb5085c048d77" + } + ], + "sourceDocxSha256": "14a355b44c436d906e41d66f0d2b1459de67f8bbb5ee3bbff4b81299ac9b0e63" + }, + { + "file": "kodierbox-inline-mixed-code-scores.docx", + "alias": "W02.docx", + "kind": "current-installed-host-export", + "host": "kodierbox", + "case": "-inline-mixed-code-scores", + "pages": 1, + "tables": 1, + "images": 1, + "formulas": 1, + "pdfSha256": "cf89d5594a9a5725e5d8d52b6136f4131d57eecb90afc4d72c1328281e77d9bc", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W02-1.png", + "pixelSha256": "ce9545c21dbcf3f215e12e11838fe9959d005226d5fb84d934a041a9ae80ef55" + } + ], + "sourceDocxSha256": "43e1346c398c201c846e388320c2fd1fc60b94ce559015f910cbe9acfd73f527" + }, + { + "file": "kodierbox-inline-mixed-general-no-scores.docx", + "alias": "W03.docx", + "kind": "current-installed-host-export", + "host": "kodierbox", + "case": "-inline-mixed-general-no-scores", + "pages": 1, + "tables": 1, + "images": 1, + "formulas": 1, + "pdfSha256": "6b7bc9fd1928da6e89a23d71f99fc2fb8140250d3d9095a8793724d70069e595", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W03-1.png", + "pixelSha256": "77a8b023881d2807ad33a333aa1ee2415d5e8bc0f208a01232ad15f4304a261f" + } + ], + "sourceDocxSha256": "454e829bf713c09c46e2afa370b2dab466e95e887df39a747240341a1dd99e67" + }, + { + "file": "kodierbox-inline-mixed-general-scores.docx", + "alias": "W04.docx", + "kind": "current-installed-host-export", + "host": "kodierbox", + "case": "-inline-mixed-general-scores", + "pages": 1, + "tables": 1, + "images": 1, + "formulas": 1, + "pdfSha256": "04c189c12a541150b06046d7fba67d04848d6614a8882de2099fb7ff4452485b", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W04-1.png", + "pixelSha256": "82ba35302cf2ac7fb0926a81180da7c1a53dc5914212a072f92e22db1abf87e8" + } + ], + "sourceDocxSha256": "36759ec7da1f3ef08a605a0bd1ccf0a38e66c73480478e1d4ef8f23f489ee8ec" + }, + { + "file": "kodierbox-rich-no-scores.docx", + "alias": "W05.docx", + "kind": "current-installed-host-export", + "host": "kodierbox", + "case": "-rich-no-scores", + "pages": 3, + "tables": 17, + "images": 1, + "formulas": 2, + "pdfSha256": "5064794591e5b6ace57fd7044a3ef8e1b917390e78ca6dcd6c58ff6a777dbe00", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W05-1.png", + "pixelSha256": "3fbc832ae1dfdce521572a47f778621de5186ef3a1fd9023efa50f36aa2d7db3" + }, + { + "page": 2, + "png": "W05-2.png", + "pixelSha256": "0f427c0658058681379a5c7ee5140b069634061fa43acb4e4533f114048783fe" + }, + { + "page": 3, + "png": "W05-3.png", + "pixelSha256": "9a2bbd69baf68863c94f0615dee422d7f265e4ebcb624fd558bddffc35abd01c" + } + ], + "sourceDocxSha256": "bfc4926373af41b1fe7f5f51078072f0a6723dafe53dc04bd2f84111b27aee8b" + }, + { + "file": "kodierbox-rich-scores.docx", + "alias": "W06.docx", + "kind": "current-installed-host-export", + "host": "kodierbox", + "case": "-rich-scores", + "pages": 3, + "tables": 17, + "images": 1, + "formulas": 2, + "pdfSha256": "96c407831bf58fd0dd23866a96376ceeb1b5725bc074767c7ea4844a0a7d0bb9", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W06-1.png", + "pixelSha256": "eaca14c87498e9c4b67b569801974300109a45829469ab985185aaac2a52ca5f" + }, + { + "page": 2, + "png": "W06-2.png", + "pixelSha256": "99c338d92c958145d928171931f8052e3481bd20fa457e47156bc44fb1c4850d" + }, + { + "page": 3, + "png": "W06-3.png", + "pixelSha256": "7a3eb16bd6b8cb3582a1bf15aac7f3b5c30c9df50b78537b1fef75b2ca80150e" + } + ], + "sourceDocxSha256": "eab8f321594af182ca5fb05d4fef089bbe42febbead418d46f263bddbd328222" + }, + { + "file": "kodierbox.docx", + "alias": "W07.docx", + "kind": "current-installed-host-export", + "host": "kodierbox", + "case": "", + "pages": 3, + "tables": 22, + "images": 2, + "formulas": 2, + "pdfSha256": "532c19126a615d8841a39d6e62d3b16eb344a0e9831df34d16e79f6574a2a5b1", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W07-1.png", + "pixelSha256": "3b78883b3259ae418ecfb27e55873824520220e2a369866c04f61d7c77d12828" + }, + { + "page": 2, + "png": "W07-2.png", + "pixelSha256": "e114004af7cf5d9da822137a91bf4f4310a37feb4253712ec6643006b6e0c885" + }, + { + "page": 3, + "png": "W07-3.png", + "pixelSha256": "3321bba5e3311035f064352c363945cfce3c6a134a6a501f9619173e8b555c9d" + } + ], + "sourceDocxSha256": "7d56e821244e00a9aa0fb1b65793165f4e1764d7f3619d88e65087e282dca35a" + }, + { + "file": "studio-inline-mixed-code-no-scores.docx", + "alias": "W08.docx", + "kind": "current-installed-host-export", + "host": "studio", + "case": "-inline-mixed-code-no-scores", + "pages": 1, + "tables": 1, + "images": 1, + "formulas": 1, + "pdfSha256": "34a720e6d54998f4a318a9795e8314ad022712953c7877683964277c1efc2114", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W08-1.png", + "pixelSha256": "4e6105af72140d0033d546706a6e337aaa6def96982bd7ed09feb5085c048d77" + } + ], + "sourceDocxSha256": "6b0a9aacf4c34c67b0569b5b0f0bcf426d7f46606a00079d9ee42e615dd9046c" + }, + { + "file": "studio-inline-mixed-code-scores.docx", + "alias": "W09.docx", + "kind": "current-installed-host-export", + "host": "studio", + "case": "-inline-mixed-code-scores", + "pages": 1, + "tables": 1, + "images": 1, + "formulas": 1, + "pdfSha256": "66379ab89348ba8cb34bf523dfafe8cf2b73b6214a08f0830f75ab3a9f3f7cc0", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W09-1.png", + "pixelSha256": "ce9545c21dbcf3f215e12e11838fe9959d005226d5fb84d934a041a9ae80ef55" + } + ], + "sourceDocxSha256": "a82cb102c4eea0800643a91a568b11bf7210cfca3c83b39832ea88b067a401db" + }, + { + "file": "studio-inline-mixed-general-no-scores.docx", + "alias": "W10.docx", + "kind": "current-installed-host-export", + "host": "studio", + "case": "-inline-mixed-general-no-scores", + "pages": 1, + "tables": 1, + "images": 1, + "formulas": 1, + "pdfSha256": "c1609764365071e2cbab3df158a8e46fb688afbef6ef42b1b4e4c547598f33ea", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W10-1.png", + "pixelSha256": "77a8b023881d2807ad33a333aa1ee2415d5e8bc0f208a01232ad15f4304a261f" + } + ], + "sourceDocxSha256": "563503572fe3ec1d5ea8d1c86f9e706de25c823247a9dff2ca1eacc7f74f6792" + }, + { + "file": "studio-inline-mixed-general-scores.docx", + "alias": "W11.docx", + "kind": "current-installed-host-export", + "host": "studio", + "case": "-inline-mixed-general-scores", + "pages": 1, + "tables": 1, + "images": 1, + "formulas": 1, + "pdfSha256": "6177f92ab9f2a7af1c95e1c17d6ddf58beaa21ef61734647d520f8fd2f691d0d", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W11-1.png", + "pixelSha256": "82ba35302cf2ac7fb0926a81180da7c1a53dc5914212a072f92e22db1abf87e8" + } + ], + "sourceDocxSha256": "d0bb36fbda13433e8d68a6af7f02f40258d7f65ec6dc21851b81d97ff53ef7f4" + }, + { + "file": "studio-rich-no-scores.docx", + "alias": "W12.docx", + "kind": "current-installed-host-export", + "host": "studio", + "case": "-rich-no-scores", + "pages": 3, + "tables": 17, + "images": 1, + "formulas": 2, + "pdfSha256": "f8670c421a85992530ceb05459051a80112d1ee16716b66e414c4b56828f1ee3", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W12-1.png", + "pixelSha256": "3fbc832ae1dfdce521572a47f778621de5186ef3a1fd9023efa50f36aa2d7db3" + }, + { + "page": 2, + "png": "W12-2.png", + "pixelSha256": "0f427c0658058681379a5c7ee5140b069634061fa43acb4e4533f114048783fe" + }, + { + "page": 3, + "png": "W12-3.png", + "pixelSha256": "9a2bbd69baf68863c94f0615dee422d7f265e4ebcb624fd558bddffc35abd01c" + } + ], + "sourceDocxSha256": "7437df40c1d2018730f0e0241af9b30c5882975b7b4960e7fe83b2389bc51504" + }, + { + "file": "studio-rich-scores.docx", + "alias": "W13.docx", + "kind": "current-installed-host-export", + "host": "studio", + "case": "-rich-scores", + "pages": 3, + "tables": 17, + "images": 1, + "formulas": 2, + "pdfSha256": "ff254542288b6d1e01910f3ad2d63d3307b6e82a7342ea0482b060d3f4691e0c", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W13-1.png", + "pixelSha256": "eaca14c87498e9c4b67b569801974300109a45829469ab985185aaac2a52ca5f" + }, + { + "page": 2, + "png": "W13-2.png", + "pixelSha256": "99c338d92c958145d928171931f8052e3481bd20fa457e47156bc44fb1c4850d" + }, + { + "page": 3, + "png": "W13-3.png", + "pixelSha256": "7a3eb16bd6b8cb3582a1bf15aac7f3b5c30c9df50b78537b1fef75b2ca80150e" + } + ], + "sourceDocxSha256": "4a775f0d4d03e9f636822ebe4a0d8a7fc1f9631d70df22450a71c7bb36a24c13" + }, + { + "file": "studio.docx", + "alias": "W14.docx", + "kind": "current-installed-host-export", + "host": "studio", + "case": "", + "pages": 3, + "tables": 22, + "images": 2, + "formulas": 2, + "pdfSha256": "8adb7a4796dce0681f8368ca9c2ca988f6e7b5c99685820ade011d7a041924a9", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W14-1.png", + "pixelSha256": "3b78883b3259ae418ecfb27e55873824520220e2a369866c04f61d7c77d12828" + }, + { + "page": 2, + "png": "W14-2.png", + "pixelSha256": "e114004af7cf5d9da822137a91bf4f4310a37feb4253712ec6643006b6e0c885" + }, + { + "page": 3, + "png": "W14-3.png", + "pixelSha256": "3321bba5e3311035f064352c363945cfce3c6a134a6a501f9619173e8b555c9d" + } + ], + "sourceDocxSha256": "d31af628f75d20c1c5bce4ea3cbbf94c90f283be9c01540d8aa3928ed602ea57" + }, + { + "file": "original-studio-scores.docx", + "alias": "W15.docx", + "kind": "independent-pre-harmonization-studio-reference", + "host": "original-studio", + "case": "-rich-scores", + "pages": 3, + "tables": 17, + "images": 1, + "formulas": 2, + "pdfSha256": "e50180d4ad1c23fb39189242e3c0947e3e988ed33f6881bd1a8c85c87ac9bad8", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W15-1.png", + "pixelSha256": "13ad9d0b6375ae793a4ed5904c039bffd0f8e92ab7aca257cc4264a14c92daff" + }, + { + "page": 2, + "png": "W15-2.png", + "pixelSha256": "d9e22a2c746246f1527304314c6d343d86154f95a1f2d832424658a860bab2f2" + }, + { + "page": 3, + "png": "W15-3.png", + "pixelSha256": "10d245f8f284a447cceeed8de0095da430f6009ce4de024d3624c0f54c1e0bef" + } + ], + "sourceDocxSha256": "bd189230eafbb8484e1bbc84c9ae87a7136f9a093fe68eefff99dc2e4dba1d6f" + }, + { + "file": "original-studio-no-scores.docx", + "alias": "W16.docx", + "kind": "independent-pre-harmonization-studio-reference", + "host": "original-studio", + "case": "-rich-no-scores", + "pages": 3, + "tables": 17, + "images": 1, + "formulas": 2, + "pdfSha256": "d6c990a340d7c6b71e75c961802fd8d0d2a566b7724ff738ab08966c4af0dcfe", + "creator": "Microsoft\u00ae Word LTSC", + "producer": "Microsoft\u00ae Word LTSC", + "renderedPages": [ + { + "page": 1, + "png": "W16-1.png", + "pixelSha256": "e22e64e8555ea52583e4fda4e04b5202ca925d4d98053db3c5c919f47b1eca55" + }, + { + "page": 2, + "png": "W16-2.png", + "pixelSha256": "cbf0c55cb64899d584b88ace9478ea647370ced27a7f5cd650bb911de0350623" + }, + { + "page": 3, + "png": "W16-3.png", + "pixelSha256": "0466a39ffb07ca89e678ad931f6313d683e62cfc91db797fb6c4cbf12593d431" + } + ], + "sourceDocxSha256": "3cda8fc961dfed5afa6b4fa0c5e723a5855033fc7f031e526ccd92b848cfaea6" + } + ], + "pairs": [ + { + "case": "-inline-mixed-code-no-scores", + "files": [ + "kodierbox-inline-mixed-code-no-scores.docx", + "studio-inline-mixed-code-no-scores.docx" + ], + "pagesPerHost": 1, + "identicalRasterPixels": true + }, + { + "case": "-inline-mixed-code-scores", + "files": [ + "kodierbox-inline-mixed-code-scores.docx", + "studio-inline-mixed-code-scores.docx" + ], + "pagesPerHost": 1, + "identicalRasterPixels": true + }, + { + "case": "-inline-mixed-general-no-scores", + "files": [ + "kodierbox-inline-mixed-general-no-scores.docx", + "studio-inline-mixed-general-no-scores.docx" + ], + "pagesPerHost": 1, + "identicalRasterPixels": true + }, + { + "case": "-inline-mixed-general-scores", + "files": [ + "kodierbox-inline-mixed-general-scores.docx", + "studio-inline-mixed-general-scores.docx" + ], + "pagesPerHost": 1, + "identicalRasterPixels": true + }, + { + "case": "-rich-no-scores", + "files": [ + "kodierbox-rich-no-scores.docx", + "studio-rich-no-scores.docx" + ], + "pagesPerHost": 3, + "identicalRasterPixels": true + }, + { + "case": "-rich-scores", + "files": [ + "kodierbox-rich-scores.docx", + "studio-rich-scores.docx" + ], + "pagesPerHost": 3, + "identicalRasterPixels": true + }, + { + "case": "", + "files": [ + "kodierbox.docx", + "studio.docx" + ], + "pagesPerHost": 3, + "identicalRasterPixels": true + } + ], + "visualInspectionNotes": [ + "All 32 pages of the native Windows Word PDFs were visually inspected at 120 DPI.", + "No unexpected clipping, overlapping content, missing equations/images or pagination errors were observed.", + "Old Studio rich references retain three pages with and without scores. Pages 1 and 2 have pixel-identical bodies (footer excluded).", + "Page 3 intentionally honors the explicit 18pt Farbe instruction, making that row taller and shifting subsequent content. Footer branding and generated dates intentionally differ." + ], + "independentStudioReference": { + "revision": "2504265721a59c7c65425072951ce82ab16f0fb8", + "bodyComparison": [ + { + "reference": "W15", + "current": "W13", + "page": 1, + "identicalBodyPixels": true, + "differenceBoundingBox": null, + "expectedDifference": null + }, + { + "reference": "W15", + "current": "W13", + "page": 2, + "identicalBodyPixels": true, + "differenceBoundingBox": null, + "expectedDifference": null + }, + { + "reference": "W15", + "current": "W13", + "page": 3, + "identicalBodyPixels": false, + "differenceBoundingBox": [ + 118, + 129, + 875, + 686 + ], + "expectedDifference": "Explicit 18pt font-size correction and consequent vertical displacement; visually checked." + }, + { + "reference": "W16", + "current": "W12", + "page": 1, + "identicalBodyPixels": true, + "differenceBoundingBox": null, + "expectedDifference": null + }, + { + "reference": "W16", + "current": "W12", + "page": 2, + "identicalBodyPixels": true, + "differenceBoundingBox": null, + "expectedDifference": null + }, + { + "reference": "W16", + "current": "W12", + "page": 3, + "identicalBodyPixels": false, + "differenceBoundingBox": [ + 118, + 129, + 875, + 686 + ], + "expectedDifference": "Explicit 18pt font-size correction and consequent vertical displacement; visually checked." + } + ], + "footerExclusionPixels": { + "bottomFromY": 1280, + "imageHeight": 1404, + "dpi": 120 + } + }, + "packageSha256": "2df79a7b18196d7c25d7e02e15d5b148200d75ba2a315214ccfb8386536dd0a6", + "testedRevisions": { + "library": "14432a346e75acdaabfd3394cdacb07b4dd65f29", + "studio": "3b5fd4c379e73589a5d05575d459f2050f4ed33f", + "box": "eaafc1a0b96a0316db301a48cf2bd8bd2762dbc5" + }, + "officeEdition": "Microsoft Office LTSC Professional Plus 2024", + "officeVersionVerifiedInUi": "2408 (Build 16.0.17932.21000), 64 bit", + "rasterComparisonDpi": 120, + "repositoryScriptRepeat": { + "automatedChecksPassed": true, + "documents": 16, + "pages": 32, + "allRenderedPagesIdenticalToVisuallyInspectedRun": true, + "scriptSha256": "24df6fa15f882efa396bd44444ca407c3c87a7c233e6a50a55d3b1c0b0db8d40", + "checkedAtUtc": "2026-10-15T15:41:44.2111512Z" + } +} diff --git a/docs/codebook-word-windows-2026-10-15.md b/docs/codebook-word-windows-2026-10-15.md new file mode 100644 index 0000000..629d99f --- /dev/null +++ b/docs/codebook-word-windows-2026-10-15.md @@ -0,0 +1,118 @@ +# Codebook: Abnahme in Windows-Desktop-Word am 15.10.2026 + +**Bestanden für den unten bezeichneten Paketstand und die 16 synthetischen +Prüfdokumente.** Die Dokumente wurden in installiertem Windows-Desktop-Word +regulär und schreibgeschützt geöffnet, geprüft und durch Word als PDF exportiert. +Alle 32 PDF-Seiten wurden gerastert und vollständig visuell geprüft. +Es handelt sich um einen zusätzlichen Desktop-Word-Nachweis zur bisherigen +Generator-, Installations- und Anwendungsprüfung. + +## Umgebung und Herkunft + +- Microsoft Office LTSC Professional Plus 2024, Word Version 2408, + Build **16.0.17932.21000**, 64 Bit; Edition und vollständiger Build in Word geprüft. +- Aktueller gemeinsamer 4.1.0-Tarball in beiden Hosts, SHA256 + `2df79a7b18196d7c25d7e02e15d5b148200d75ba2a315214ccfb8386536dd0a6`. +- 14 Exporte aus dem letzten Installationsvergleich: je sieben aus den echten + Studio- und Kodierbox-Anwendungsadaptern mit Datenquellen im Speicher. +- Zwei unveränderte Rich-Text-Dokumente der unabhängigen alten Studio-Referenz + `2504265721a59c7c65425072951ce82ab16f0fb8`, mit und ohne Scores. + Diese Referenzen wurden nicht durch den gemeinsamen Generator neu erzeugt. +- Ausschließlich künstliche Daten; keine Dateien aus den bereitgestellten + Aufgabenarchiven als feste Referenzen verwendet. + +Getestete Quellstände, Dokument-/PDF-Prüfsummen und Seitenbild-Prüfsummen stehen +im [maschinenlesbaren Bericht](codebook-word-windows-2026-10-15.json). + +## Fälle und Ergebnisse + +| Fall | Dokumente | Seiten je Dokument | Ergebnis | +| --- | ---: | ---: | --- | +| Filter, Aliase, Items, Missing 0, Code/Score 0, Ableitungen | 2 | 3 | bestanden | +| Rich Text, Regeltexte, Listen, Bilder und Formeln ohne Scores | 2 | 3 | bestanden | +| Dieselben Inhalte mit Scores | 2 | 3 | bestanden | +| Defekte und gültige Inlinebilder in Codeinstruktion, ohne/mit Scores | 4 | 1 | bestanden | +| Defekte und gültige Inlinebilder in allgemeiner Instruktion, ohne/mit Scores | 4 | 1 | bestanden | +| Alte Studio-Rich-Text-Referenzen ohne/mit Scores | 2 | 3 | bestanden | + +Word bestätigt Tabellenzahl, Spaltenzahl, ungeteilte Tabellenzeilen, Überschriften, +Inlinebilder, native Gleichungen und aktualisierte PAGE-/NUMPAGES-Felder. +Text vor und nach defekten Bildern sowie nachfolgende gültige Bilder und Formeln +bleiben erhalten. Der sichtbare Fehlertext für die absichtlich defekten Bilder +ist erwartet. Alle DOCX-Quellprüfsummen bleiben unverändert; es wurde weder +OpenAndRepair verwendet noch in die Quellen gespeichert. + +Alle sieben aktuellen Studio-/Kodierbox-Paare haben identischen Word-Text, +identische Seitenzahl und Fußzeilen. Die vollständigen Seitenbilder sind bei +120 DPI **pixelgleich**, einschließlich Fußzeilen und Seitenfeldern. +Alle 32 PDF-Seiten zeigen die korrekte Angabe „Seite X von Y“. + +Die Sichtprüfung umfasst Spaltenbreiten, Zeilen- und Seitenumbrüche, lange +Überschriften, Bilder, Formeln, Listen, Hervorhebungen und Fußzeilen. Es wurden +keine unerwarteten abgeschnittenen oder überlappenden Inhalte gefunden. +Der vorhandene Studio-Umbruch einer vollständigen großen Codezeile auf die +Folgeseite bleibt erhalten. + +## Vergleich mit der alten Studio-Ausgabe + +Die beiden alten Rich-Text-Dokumente haben weiterhin je drei Seiten. Die +Seitenkörper von Seite 1 und 2 sind jeweils pixelgleich zur aktuellen Ausgabe; +für diesen separaten Vergleich ist ausschließlich der Fußzeilenbereich +ausgenommen. Die Fußzeilen werden unabhängig auf Kennzeichnung und Seitenzahlen +geprüft. + +Auf Seite 3 wird die bereits dokumentierte explizite **18pt-Schriftgröße** der +synthetischen Instruktion „Farbe“ nun korrekt dargestellt. Dadurch werden diese +Tabellenzeile höher und die folgenden Inhalte nach unten verschoben. Die +Seitenzahl bleibt gleich. Weitere sichtbare Änderungen sind die vereinbarte +Kennzeichnung **„IQB Codebook“** und das Erstellungsdatum. Diese gezielten +Korrekturen sind keine pixelgleiche Bewahrung des alten Fehlverhaltens. +Weitere unerwartete Unterschiede wurden in diesen Referenzen nicht gefunden. + +## Reproduzieren + +Der Installationsvergleich erzeugt sieben Fälle je Host und +`word-expectations.json`. Optional die beiden unveränderten alten Referenzen +als `original-studio-scores.docx` und `original-studio-no-scores.docx` in denselben +Ordner kopieren. Auf Windows mit installiertem Desktop-Word: + +```powershell +./scripts/verify-codebook-word-windows.ps1 -Directory C:\Codebook-Pruefung -IncludeStudioReferences +``` + +Ohne alte Referenzen den Schalter weglassen. Das Skript unterstützt auch die +frühere vollständige Drei-Fall-Matrix. Es verändert keine Sicherheits- oder +Ausführungsrichtlinien und arbeitet mit lokalen temporären Kopien, auch wenn +die Eingaben auf einem Netzlaufwerk liegen. Es verwendet typisierte COM-Argumente +und indexierten Zugriff auf Tabellen und Fußzeilenfelder; dadurch werden die +auf der geprüften Word-Installation beobachteten COM-Fehler und Hänger vermieden. +Eine eigene Word-Instanz mit bereits offenen Dokumenten wird nicht verwendet +oder geschlossen. Fortschrittslog und abschließender JSON-Bericht werden erst +nach Abschluss veröffentlicht, um SMB-Lesesperren während des Schreibens zu vermeiden. + +PDF-Prüfung und Rastervergleich, mit Python 3, Pillow, pypdf und Poppler im PATH: + +```shell +python3 scripts/verify-codebook-word-pdfs.py /path/to/WordResults-folder +``` + +Das PowerShell-Skript und der PDF-Prüfer setzen die visuelle Abnahme ausdrücklich +nicht automatisch auf bestanden. Alle Seiten anschließend tatsächlich sichten. +Die endgültige eingecheckte PowerShell-Fassung wurde auf demselben Windows-Rechner +erneut mit allen 16 Dateien ausgeführt: automatische Prüfungen bestanden, +Quellprüfsummen unverändert, alle 32 gerenderten Seiten pixelgleich zum gesichteten +ersten Lauf. Dieser Wiederholungslauf ist im JSON-Bericht separat ausgewiesen. + +## Nachweisgrenzen + +Dieser Nachweis gilt für den bezeichneten Paketstand, die beschriebenen Fälle +und diese Desktop-Word-Version. Er ist keine Zusage für jede zukünftige +Word-Version oder beliebige Eingabedokumente. Die breitere fachliche Matrix +bleibt durch die Generator- und Installationsvergleiche abgesichert. + +Browserdialoge, direkte HTTP-Downloads, Redis-/Jobabläufe und Berechtigungen +werden durch diese Word-Abnahme nicht erneut geprüft. Sie benötigen weiterhin +eigene Anwendungsprüfungen. Es wurden hier nur Prüfscripte und Dokumentation +geändert; kein Generator-, Schemer-, Validierungs- oder Kodierengine-Code. +Es erfolgten kein PR-Merge, npm-Release oder Deployment. Entfernte CI bleibt +ein separater Nachweis. diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md index e40fc35..f543d10 100644 --- a/docs/shared-codebook.md +++ b/docs/shared-codebook.md @@ -1,5 +1,14 @@ # Gemeinsames Codebook: Integrationsvertrag +## Windows-Desktop-Word-Abnahme des aktuellen Paketstands + +Die [Abnahme vom 15.10.2026](codebook-word-windows-2026-10-15.md) bestätigt +16 synthetische Dokumente mit 32 vollständig gesichteten Seiten in Word LTSC 2024 +(Build 16.0.17932.21000, 64 Bit). Alle sieben aktuellen Studio-/Kodierbox-Paare +sind pixelgleich; die unveränderten alten Studio-Referenzen bleiben abgesehen +von vereinbarter Kennzeichnung und dokumentierter 18pt-Korrektur erhalten. +Der geprüfte Tarball ist der nachfolgend bezeichnete aktuelle SOLVER-Paketstand. + ## Aktuelle SOLVER-Kompatibilität und Mindestversion Der [Nachtrag vom 15.10.2026](codebook-solver-compatibility-2026-10-15.md) erhält @@ -151,8 +160,9 @@ Details und Grenzen: [Abnahmeprotokoll](./codebook-acceptance-2026-10-13.md). XML-Gleichheit und LibreOffice-Rendering allein ersetzen keinen Office-Nachweis. Für einen zusätzlichen Windows-Desktop-Word-Abnahmeweg legt der Installationsvergleich künstliche Exporte -sechs DOCX-Dateien und `word-expectations.json` im Ausgabeverzeichnis ab: je Anwendung -einen Filter-/Missing-0-Fall sowie Rich-Text-Fälle mit und ohne Score-Spalte. Die +14 DOCX-Dateien und `word-expectations.json` im Ausgabeverzeichnis ab: je Anwendung +einen Filter-/Missing-0-Fall, Rich-Text-Fälle mit und ohne Score-Spalte sowie +vier Inlinebild-Fälle mit defekten und nachfolgenden gültigen Inhalten. Die Rich-Text-Fälle werden zusätzlich direkt gegen die alte Studio-Referenz geprüft. Auf einem Windows-Rechner mit Desktop-Word: @@ -162,10 +172,12 @@ Auf einem Windows-Rechner mit Desktop-Word: Das Skript öffnet alle Dateien regulär und schreibgeschützt, aktualisiert Felder, prüft Textgleichheit, Seitenzahl, Tabellen, ungeteilte Zeilen, Bilder, Formeln und -Fußzeilen und exportiert sechs PDFs. Es speichert Word-Version und Build im Ergebnis. +Fußzeilen und exportiert 14 PDFs (mit optionalen alten Referenzen 16). Es speichert Word-Version und Build im Ergebnis. Anschließend alle gerenderten Dokumente vollständig visuell prüfen: insbesondere Spaltenbreiten, Umbrüche, Formeln, Bilder, Schriften und „Seite X von Y“. `visualAcceptanceComplete` bleibt bis zur tatsächlichen visuellen Abnahme false. +Die [tatsächliche Windows-Abnahme](codebook-word-windows-2026-10-15.md) enthält +auch die beiden alten Studio-Referenzen und einen vollständigen Wiederholungslauf. Eine nicht ausgeführte Windows-Prüfung darf nicht als bestanden gemeldet werden. ### Editor-/Player-Abgleich und Formatkombinationen