-
-
-
-
-
-
- 0"
- [indeterminate]="unitList().length > 0 && unitList().length < availableUnits().length"
- (change)="toggleAllUnits($event.checked)"
- color="primary">
- {{'coding.select-all-units' | translate}}
-
-
-
-
-
-
- {{'search' | translate}}
-
- search
- @if (filterValue()) {
-
- }
-
-
-
-
- @if (isLoading()) {
-
-
-
{{'loading-units' | translate}}
-
- }
-
-
- @if (!isLoading()) {
-
-
-
-
- |
-
-
-
- |
-
-
-
-
- {{'coding.unit-name' | translate}} |
- {{formatUnitName(unit.unitName)}} |
-
-
-
-
-
-
-
- |
- @if (filterValue()) {
- {{'no-units-matching' | translate}} "{{filterValue()}}"
- } @else {
- {{'no-units-available' | translate}}
- }
- |
-
-
-
- }
-
-
-
-
+
-
+
{{'coding.codebook-quick-filters' | translate}}
-
- {{'coding.training-requirement-filter' | translate}}
-
- {{'coding.training-requirement-all' | translate}}
- {{'coding.training-requirement-required' | translate}}
- {{'coding.training-requirement-not-required' | translate}}
-
-
-
{{'coding.job-definition-filter' | translate}}
@@ -142,136 +49,10 @@
{{'coding.codebook-quick-filters' | translate}}
-
- {{'coding.variable-bundle-filter' | translate}}
-
- @for (variableBundle of availableVariableBundles(); track variableBundle.id) {
-
- {{ variableBundle.name }}
-
- }
- @if (!isLoadingVariableBundles() && availableVariableBundles().length === 0) {
- {{'coding.variable-bundle-none' | translate}}
- }
-
-
-
-
-
-
-
{{'coding.codebook-content' | translate}}
-
-
- {{'coding.has-only-vars-with-codes' | translate}}
-
-
-
- {{'coding.has-general-instructions' | translate}}
-
-
-
- {{'coding.hide-item-var-relation' | translate}}
-
-
-
- {{'coding.has-derived-vars' | translate}}
-
-
-
- {{'coding.has-only-manual-coding' | translate}}
-
-
-
- {{'coding.has-closed-vars' | translate}}
-
-
-
- {{'coding.show-score' | translate}}
-
-
-
- {{'coding.code-label-to-upper' | translate}}
-
-
-
-
-
-
-
-
-
{{ 'workspace.coding-missing-profiles' | translate }}
-
- {{'workspace.select-missings-profile' | translate }}
-
- @for (missingsProfile of missingsProfiles(); track missingsProfile) {
-
- @if (missingsProfile.id === 0) {
- {{ 'Keines' }}
- } @else {
- {{missingsProfile.label}}
- }
-
- }
-
-
-
-
-
-
-
-
-
{{'coding.export-format' | translate}}
-
- JSON
- DOCX
-
-
-
- @if(workspaceChanges()) {
-
-
- {{'coding.error-save-changes' | translate}}
-
- }
-
-
-
+
@if (codebookJobStatus() !== 'idle') {
@@ -309,18 +90,5 @@
{{'coding.export-format' | translate}}
}
-
-
-
-
+
diff --git a/apps/frontend/src/app/coding/components/export-coding-book/export-coding-book.component.spec.ts b/apps/frontend/src/app/coding/components/export-coding-book/export-coding-book.component.spec.ts
index 26a72acfc..e8f1d24dc 100644
--- a/apps/frontend/src/app/coding/components/export-coding-book/export-coding-book.component.spec.ts
+++ b/apps/frontend/src/app/coding/components/export-coding-book/export-coding-book.component.spec.ts
@@ -1,6 +1,10 @@
-import { ComponentFixture, TestBed } from '@angular/core/testing';
+import {
+ ComponentFixture, TestBed
+} from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { CodebookExportComponent } from '@iqb/ngx-coding-components/codebook-export';
import { DatePipe } from '@angular/common';
-import { MatDialog } from '@angular/material/dialog';
+import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import {
of, throwError, BehaviorSubject, Subject
} from 'rxjs';
@@ -110,6 +114,7 @@ describe('ExportCodingBookComponent', () => {
const mockValidationResults$ = new BehaviorSubject
(null);
beforeEach(async () => {
+ jest.useFakeTimers({ doNotFake: ['queueMicrotask', 'requestAnimationFrame'] });
const exportServiceMock = {
getCodingBook: jest.fn(),
startCodebookJob: jest.fn(),
@@ -165,6 +170,7 @@ describe('ExportCodingBookComponent', () => {
await TestBed.configureTestingModule({
imports: [ExportCodingBookComponent],
providers: [
+ { provide: MatDialogRef, useValue: { close: jest.fn() } },
{ provide: CodingExportService, useValue: exportServiceMock },
{ provide: CodingJobBackendService, useValue: codingJobBackendServiceMock },
{ provide: MissingsProfileService, useValue: missingsProfileServiceMock },
@@ -214,7 +220,9 @@ describe('ExportCodingBookComponent', () => {
afterEach(() => {
mockValidationProgress$.next({ status: 'idle', progress: 0, message: '' });
mockValidationResults$.next(null);
+ fixture.destroy();
jest.restoreAllMocks();
+ jest.useRealTimers();
});
it('should create', () => {
@@ -695,6 +703,72 @@ describe('ExportCodingBookComponent', () => {
});
});
+ it('keeps host scope filters when the shared form requests an export', () => {
+ component.selectedJobDefinitionId.set(10);
+ component.selectedVariableBundleIds.set([5]);
+ component.contentOptions.update(options => ({ ...options, trainingRequirement: 'required' }));
+ exportService.startCodebookJob.mockReturnValue(new Subject());
+ component.onSharedExport({
+ selectedUnits: [2],
+ missingsProfileId: 1,
+ contentOptions: { ...component.contentOptions(), exportFormat: 'json' }
+ });
+ component.onSharedExport({
+ selectedUnits: [1],
+ missingsProfileId: 2,
+ contentOptions: { ...component.contentOptions(), exportFormat: 'docx' }
+ });
+ expect(exportService.startCodebookJob).toHaveBeenCalledTimes(1);
+ expect(exportService.startCodebookJob).toHaveBeenCalledWith(1, '1', expect.objectContaining({
+ exportFormat: 'json', trainingRequirement: 'required', jobDefinitionId: 10, variableBundleIds: [5]
+ }), [2]);
+ });
+
+ it.each(['required', 'not-required'] as const)('preserves %s from filter change through the rendered shared form to job start', trainingRequirement => {
+ fixture.detectChanges();
+ const shared = fixture.debugElement.query(By.directive(CodebookExportComponent)).componentInstance as CodebookExportComponent;
+ shared.contentOptions.trainingRequirement = trainingRequirement;
+ expect(shared.showGroupColumn).toBe(false);
+ expect(fixture.nativeElement.textContent).not.toContain('coding.variable-bundle-filter');
+ expect(fixture.nativeElement.querySelector('mat-select[multiple]')).toBeNull();
+ fixture.detectChanges();
+ shared.toggleAll();
+ exportService.startCodebookJob.mockReturnValue(new Subject());
+ shared.exportCodingBook();
+ expect(exportService.startCodebookJob).toHaveBeenCalledWith(
+ 1,
+ expect.any(String),
+ expect.objectContaining({ trainingRequirement }),
+ expect.arrayContaining([1])
+ );
+ });
+
+ it('does not overlap slow status requests and cancels them when the dialog closes', () => {
+ const status = new Subject<{ status: string; progress: number }>();
+ exportService.startCodebookJob.mockReturnValue(of({ jobId: 'job', message: 'ok' }));
+ exportService.getCodebookJobStatus.mockReturnValue(status);
+ component.unitList.set([1]); component.exportCodingBook();
+ jest.advanceTimersByTime(4500);
+ expect(exportService.getCodebookJobStatus).toHaveBeenCalledTimes(1);
+ expect(status.observed).toBe(true);
+ component.ngOnDestroy();
+ expect(status.observed).toBe(false);
+ jest.advanceTimersByTime(4500);
+ expect(exportService.getCodebookJobStatus).toHaveBeenCalledTimes(1);
+ });
+
+ it('keeps export locked until the completed job has been downloaded', () => {
+ exportService.startCodebookJob.mockReturnValue(of({ jobId: 'job', message: 'ok' }));
+ exportService.getCodebookJobStatus.mockReturnValue(of({ status: 'completed', progress: 100 }));
+ const download = new Subject();
+ exportService.downloadCodebookFile.mockReturnValue(download);
+ component.unitList.set([1]); component.exportCodingBook(); jest.advanceTimersByTime(1500);
+ component.exportCodingBook();
+ expect(component.isExportBusy).toBe(true);
+ expect(exportService.startCodebookJob).toHaveBeenCalledTimes(1);
+ component.ngOnDestroy(); expect(download.observed).toBe(false);
+ });
+
describe('resetCodebookJob', () => {
it('should reset all codebook job state', () => {
component.codebookJobId.set('123');
diff --git a/apps/frontend/src/app/coding/components/export-coding-book/export-coding-book.component.ts b/apps/frontend/src/app/coding/components/export-coding-book/export-coding-book.component.ts
index 148cb7994..1c3ebaa68 100644
--- a/apps/frontend/src/app/coding/components/export-coding-book/export-coding-book.component.ts
+++ b/apps/frontend/src/app/coding/components/export-coding-book/export-coding-book.component.ts
@@ -1,39 +1,36 @@
import {
- Component, OnInit, OnDestroy, computed, signal, ChangeDetectionStrategy, DestroyRef, inject
+ ChangeDetectionStrategy,
+ ChangeDetectorRef,
+ Component,
+ OnInit,
+ OnDestroy,
+ signal,
+ inject
} from '@angular/core';
-import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { CodebookExportComponent } from '@iqb/ngx-coding-components/codebook-export';
+import type { CodebookExportConfig, UnitSelectionItem } from '@iqb/ngx-coding-components/codebook-models';
import { DatePipe } from '@angular/common';
import { FormsModule } from '@angular/forms';
-import { MatDialog, MatDialogModule } from '@angular/material/dialog';
-import { MatCheckboxModule } from '@angular/material/checkbox';
-import { MatRadioModule } from '@angular/material/radio';
+import { MatDialog, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatSelectModule } from '@angular/material/select';
import { MatButtonModule } from '@angular/material/button';
-import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatOptionModule } from '@angular/material/core';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
-import { MatDividerModule } from '@angular/material/divider';
-import { MatTableDataSource, MatTableModule } from '@angular/material/table';
-import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { MatTableDataSource } from '@angular/material/table';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
+import { Subject, Subscription, interval } from 'rxjs';
import {
- Subject, Subscription, interval
-} from 'rxjs';
-import {
- debounceTime, distinctUntilChanged, switchMap, takeUntil
+ debounceTime, distinctUntilChanged, exhaustMap, takeUntil
} from 'rxjs/operators';
import {
CodeBookContentSetting,
CodebookTrainingRequirementFilter
} from '../../../../../../../api-dto/coding/codebook-content-setting';
import { CodingExportService } from '../../services/coding-export.service';
-import {
- CodingJobBackendService,
- JobDefinition
-} from '../../services/coding-job-backend.service';
+import { CodingJobBackendService, JobDefinition } from '../../services/coding-job-backend.service';
import { VariableBundle } from '../../models/coding-job.model';
import { MissingsProfileService } from '../../services/missings-profile.service';
import { FileService } from '../../../shared/services/file/file.service';
@@ -54,72 +51,53 @@ interface CodebookUnitOption {
unitAlias: string | null;
unitData: string;
}
-
@Component({
- changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'coding-box-export-coding-book',
+ changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: './export-coding-book.component.html',
styleUrls: ['./export-coding-book.component.scss'],
standalone: true,
imports: [
+ CodebookExportComponent,
FormsModule,
MatDialogModule,
- MatCheckboxModule,
- MatRadioModule,
MatSelectModule,
MatButtonModule,
- MatInputModule,
MatFormFieldModule,
MatOptionModule,
MatIconModule,
MatTooltipModule,
- MatDividerModule,
- MatTableModule,
- MatProgressSpinnerModule,
MatProgressBarModule,
TranslateModule
],
- providers: [
- DatePipe
- ]
+ providers: [DatePipe]
})
export class ExportCodingBookComponent implements OnInit, OnDestroy {
- private exportService = inject(CodingExportService);
- private codingJobBackendService = inject(CodingJobBackendService);
- private missingsProfileService = inject(MissingsProfileService);
- private fileService = inject(FileService);
- private appService = inject(AppService);
- private datePipe = inject(DatePipe);
- private validationStateService = inject(ValidationStateService);
- private translateService = inject(TranslateService);
- private dialog = inject(MatDialog);
-
- private readonly destroyRef = inject(DestroyRef);
-
readonly unitList = signal([]);
readonly availableUnits = signal([]);
-
dataSource = new MatTableDataSource([]);
-
- protected readonly filterValue = signal('');
- protected filterTextChanged = new Subject();
+ readonly filterValue = signal('');
+ filterTextChanged = new Subject();
readonly isLoading = signal(false);
-
readonly selectedMissingsProfile = signal(0);
- readonly missingsProfiles = signal<{ id: number, label: string }[]>([{ id: 0, label: '' }]);
+ readonly missingsProfiles = signal<
+ {
+ id: number;
+ label: string;
+ }[]
+ >([{ id: 0, label: '' }]);
+
readonly selectedJobDefinitionId = signal(null);
readonly availableJobDefinitions = signal([]);
readonly jobDefinitionOptions = signal([]);
- protected readonly selectedJobDefinitionLabel = signal('');
- protected readonly selectedJobDefinitionSummary = signal('');
+ readonly selectedJobDefinitionLabel = signal('');
+ readonly selectedJobDefinitionSummary = signal('');
readonly isLoadingJobDefinitions = signal(false);
- protected readonly selectedVariableBundleIds = signal([]);
+ readonly selectedVariableBundleIds = signal([]);
readonly availableVariableBundles = signal([]);
- protected readonly isLoadingVariableBundles = signal(false);
- protected readonly workspaceChanges = signal(false);
-
- protected displayedColumns: string[] = ['select', 'unitName'];
-
+ readonly isLoadingVariableBundles = signal(false);
+ readonly workspaceChanges = signal(false);
+ displayedColumns: string[] = ['select', 'unitName'];
readonly contentOptions = signal({
exportFormat: 'docx',
missingsProfile: '',
@@ -139,18 +117,77 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
private destroy$ = new Subject();
readonly validationResults = signal(null);
readonly validationProgress = signal(null);
- readonly isValidating = computed(() => {
- const status = this.validationProgress()?.status;
- return status === 'loading' || status === 'processing';
- });
-
+ readonly isValidating = signal(false);
readonly validationCacheKey = signal(null);
-
readonly codebookJobId = signal(null);
readonly codebookJobStatus = signal<'idle' | 'pending' | 'processing' | 'completed' | 'failed'>('idle');
readonly codebookJobProgress = signal(0);
readonly codebookJobError = signal(null);
private codebookPollingSubscription: Subscription | null = null;
+ private readonly exportService = inject(CodingExportService);
+ private readonly codingJobBackendService = inject(CodingJobBackendService);
+ private readonly missingsProfileService = inject(MissingsProfileService);
+ private readonly fileService = inject(FileService);
+ private readonly appService = inject(AppService);
+ private readonly datePipe = inject(DatePipe);
+ private readonly validationStateService = inject(ValidationStateService);
+ private readonly translateService = inject(TranslateService);
+ private readonly dialog = inject(MatDialog);
+ private readonly dialogRef = inject>(MatDialogRef);
+ private readonly changeDetectorRef = inject(ChangeDetectorRef);
+ private sharedUnitsSource: CodebookUnitOption[] | null = null;
+ private sharedUnitsCache: UnitSelectionItem[] = [];
+ private profilesSource:
+ | {
+ id: number;
+ label: string;
+ }[]
+ | null = null;
+
+ private profilesCache: {
+ id: number;
+ label: string;
+ }[] = [];
+
+ get sharedUnits(): UnitSelectionItem[] {
+ if (this.sharedUnitsSource !== this.availableUnits()) {
+ this.sharedUnitsSource = this.availableUnits();
+ this.sharedUnitsCache = this.availableUnits().map(unit => ({
+ unitId: unit.unitId,
+ key: unit.unitKey.replace(/\.vocs$/i, ''),
+ unitName: this.formatUnitName(unit.unitName),
+ unitAlias: unit.unitAlias
+ }));
+ }
+ return this.sharedUnitsCache;
+ }
+
+ get sharedMissingsProfiles(): {
+ id: number;
+ label: string;
+ }[] {
+ if (this.profilesSource !== this.missingsProfiles()) {
+ this.profilesSource = this.missingsProfiles();
+ this.profilesCache = this.missingsProfiles().filter(profile => profile.id !== 0);
+ }
+ return this.profilesCache;
+ }
+
+ get isExportBusy(): boolean {
+ return this.codebookJobStatus() === 'pending' || this.codebookJobStatus() === 'processing';
+ }
+
+ onSharedExport(config: CodebookExportConfig): void {
+ if (this.isExportBusy) return;
+ this.unitList.set([...config.selectedUnits]);
+ this.selectedMissingsProfile.set(config.missingsProfileId);
+ this.contentOptions.set({ ...this.contentOptions(), ...config.contentOptions });
+ this.exportCodingBook();
+ }
+
+ closeDialog(): void {
+ this.dialogRef.close(false);
+ }
ngOnInit(): void {
this.updateSelectedJobDefinitionDisplay();
@@ -159,26 +196,18 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
this.loadJobDefinitions();
this.loadVariableBundles();
this.loadUnitsWithFileIds();
-
this.filterTextChanged
- .pipe(
- debounceTime(300),
- distinctUntilChanged()
- ).pipe(takeUntilDestroyed(this.destroyRef))
+ .pipe(debounceTime(300), distinctUntilChanged(), takeUntil(this.destroy$))
.subscribe(event => {
this.applyFilter(event);
});
-
this.validationStateService.validationProgress$
.pipe(takeUntil(this.destroy$))
.subscribe(progress => this.handleValidationProgress(progress));
-
this.handleValidationProgress(this.validationStateService.getValidationProgress());
-
this.validationStateService.validationResults$
.pipe(takeUntil(this.destroy$))
.subscribe(results => this.handleValidationResults(results));
-
this.handleValidationResults(this.validationStateService.getValidationResults());
}
@@ -197,39 +226,44 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
const workspaceId = this.appService.selectedWorkspaceId;
if (workspaceId) {
this.isLoading.set(true);
-
- this.fileService.getUnitsWithFileIds(workspaceId).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
- next: units => {
- if (units && units.length > 0) {
- this.availableUnits.set(units.map((unit: { id: number; unitId: string; fileName: string; data: string }) => ({
- unitId: unit.id,
- unitKey: unit.unitId,
- unitName: unit.fileName,
- unitAlias: null,
- unitData: unit.data
- })));
- this.dataSource.data = this.availableUnits();
- this.dataSource.filterPredicate = (data, filter: string) => {
- const formattedName = this.formatUnitName(data.unitName).toLowerCase();
- return formattedName.includes(filter);
- };
-
- this.unitList.set([]);
- this.applyQuickFilterUnitSelection();
+ this.fileService
+ .getUnitsWithFileIds(workspaceId)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe({
+ next: units => {
+ this.changeDetectorRef.markForCheck();
+ if (units && units.length > 0) {
+ this.availableUnits.set(
+ units.map((unit: { id: number; unitId: string; fileName: string; data: string }) => ({
+ unitId: unit.id,
+ unitKey: unit.unitId,
+ unitName: unit.fileName,
+ unitAlias: null,
+ unitData: unit.data
+ }))
+ );
+ this.dataSource.data = this.availableUnits();
+ this.dataSource.filterPredicate = (data, filter: string) => {
+ const formattedName = this.formatUnitName(data.unitName).toLowerCase();
+ return formattedName.includes(filter);
+ };
+ this.unitList.set([]);
+ this.applyQuickFilterUnitSelection();
+ }
+ this.isLoading.set(false);
+ },
+ error: () => {
+ this.changeDetectorRef.markForCheck();
+ this.isLoading.set(false);
}
- this.isLoading.set(false);
- },
- error: () => {
- this.isLoading.set(false);
- }
- });
+ });
}
}
toggleUnitSelection(unitId: number, isSelected: boolean): void {
if (isSelected) {
if (!this.unitList().includes(unitId)) {
- this.unitList.update(unitIds => [...unitIds, unitId]);
+ this.unitList.set([...this.unitList(), unitId]);
}
} else {
this.unitList.set(this.unitList().filter(id => id !== unitId));
@@ -259,31 +293,20 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
return false;
}
- protected updateContentOption(
- key: K,
- value: CodeBookContentSetting[K]
- ): void {
- this.contentOptions.update(options => ({ ...options, [key]: value }));
- }
-
- onTrainingRequirementChange(
- trainingRequirement: CodebookTrainingRequirementFilter
- ): void {
- this.updateContentOption('trainingRequirement', trainingRequirement);
+ onTrainingRequirementChange(trainingRequirement: CodebookTrainingRequirementFilter): void {
+ this.contentOptions.update(current => ({ ...current, trainingRequirement: trainingRequirement }));
}
onJobDefinitionFilterChange(jobDefinitionId: number | null): void {
this.selectedJobDefinitionId.set(jobDefinitionId);
- this.updateContentOption('jobDefinitionId', jobDefinitionId);
+ this.contentOptions.update(current => ({ ...current, jobDefinitionId: jobDefinitionId }));
this.updateSelectedJobDefinitionDisplay();
this.applyQuickFilterUnitSelection();
}
onVariableBundleFilterChange(variableBundleIds: number[]): void {
- this.selectedVariableBundleIds.set(Array.isArray(variableBundleIds) ?
- variableBundleIds :
- []);
- this.updateContentOption('variableBundleIds', this.selectedVariableBundleIds());
+ this.selectedVariableBundleIds.set(Array.isArray(variableBundleIds) ? variableBundleIds : []);
+ this.contentOptions.update(current => ({ ...current, variableBundleIds: this.selectedVariableBundleIds() }));
this.applyQuickFilterUnitSelection();
}
@@ -291,23 +314,26 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
if (this.isLoadingJobDefinitions()) {
return;
}
-
const dialogData: CodebookJobDefinitionPickerDialogData = {
options: this.jobDefinitionOptions(),
selectedJobDefinitionId: this.selectedJobDefinitionId()
};
-
- this.dialog.open(CodebookJobDefinitionPickerDialogComponent, {
- width: '960px',
- maxWidth: '94vw',
- maxHeight: '88vh',
- autoFocus: false,
- data: dialogData
- }).afterClosed().pipe(takeUntilDestroyed(this.destroyRef)).subscribe(jobDefinitionId => {
- if (jobDefinitionId !== undefined) {
- this.onJobDefinitionFilterChange(jobDefinitionId);
- }
- });
+ this.dialog
+ .open(CodebookJobDefinitionPickerDialogComponent, {
+ width: '960px',
+ maxWidth: '94vw',
+ maxHeight: '88vh',
+ autoFocus: false,
+ data: dialogData
+ })
+ .afterClosed()
+ .pipe(takeUntil(this.destroy$))
+ .subscribe(jobDefinitionId => {
+ this.changeDetectorRef.markForCheck();
+ if (jobDefinitionId !== undefined) {
+ this.onJobDefinitionFilterChange(jobDefinitionId);
+ }
+ });
}
private getJobDefinitionLabel(jobDefinition: JobDefinition): string {
@@ -321,16 +347,24 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
const coderCount = jobDefinition.assignedCoders?.length || 0;
const caseOrderingMode = this.getJobDefinitionCaseOrderingLabel(jobDefinition);
const maxCases = jobDefinition.maxCodingCases ?
- ` · ${this.translateService.instant('coding.job-definition-max-cases', { count: jobDefinition.maxCodingCases })}` :
+ ` · ${this.translateService.instant('coding.job-definition-max-cases', {
+ count: jobDefinition.maxCodingCases
+ })}` :
'';
-
const scopeSummary = [
- this.formatCount(variableCount, 'coding.job-definition-count-variable-one', 'coding.job-definition-count-variable-many'),
+ this.formatCount(
+ variableCount,
+ 'coding.job-definition-count-variable-one',
+ 'coding.job-definition-count-variable-many'
+ ),
this.formatCount(unitCount, 'coding.job-definition-count-unit-one', 'coding.job-definition-count-unit-many'),
- this.formatCount(bundleCount, 'coding.job-definition-count-bundle-one', 'coding.job-definition-count-bundle-many'),
+ this.formatCount(
+ bundleCount,
+ 'coding.job-definition-count-bundle-one',
+ 'coding.job-definition-count-bundle-many'
+ ),
this.formatCount(coderCount, 'coding.job-definition-count-coder-one', 'coding.job-definition-count-coder-many')
].join(' · ');
-
return `${scopeSummary} · ${caseOrderingMode}${maxCases}`;
}
@@ -338,11 +372,9 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
const bundleNames = (jobDefinition.assignedVariableBundles || [])
.map(bundle => this.getHydratedVariableBundle(bundle.id)?.name || bundle.name)
.filter((name): name is string => Boolean(name));
-
if (bundleNames.length === 0) {
return '';
}
-
const visibleNames = bundleNames.slice(0, 2).join(', ');
const hiddenCount = bundleNames.length - 2;
return hiddenCount > 0 ?
@@ -355,23 +387,29 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
if (!workspaceId) {
return;
}
-
this.isLoadingJobDefinitions.set(true);
- this.codingJobBackendService.getJobDefinitions(workspaceId).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
- next: jobDefinitions => {
- this.availableJobDefinitions.set(jobDefinitions
- .filter(jobDefinition => jobDefinition.id !== undefined)
- .sort((a, b) => (a.id ?? 0) - (b.id ?? 0)));
- this.refreshJobDefinitionOptions();
- this.isLoadingJobDefinitions.set(false);
- this.applyQuickFilterUnitSelection();
- },
- error: () => {
- this.availableJobDefinitions.set([]);
- this.refreshJobDefinitionOptions();
- this.isLoadingJobDefinitions.set(false);
- }
- });
+ this.codingJobBackendService
+ .getJobDefinitions(workspaceId)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe({
+ next: jobDefinitions => {
+ this.changeDetectorRef.markForCheck();
+ this.availableJobDefinitions.set(
+ jobDefinitions
+ .filter(jobDefinition => jobDefinition.id !== undefined)
+ .sort((a, b) => (a.id ?? 0) - (b.id ?? 0))
+ );
+ this.refreshJobDefinitionOptions();
+ this.isLoadingJobDefinitions.set(false);
+ this.applyQuickFilterUnitSelection();
+ },
+ error: () => {
+ this.changeDetectorRef.markForCheck();
+ this.availableJobDefinitions.set([]);
+ this.refreshJobDefinitionOptions();
+ this.isLoadingJobDefinitions.set(false);
+ }
+ });
}
private loadVariableBundles(): void {
@@ -379,73 +417,72 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
if (!workspaceId) {
return;
}
-
this.isLoadingVariableBundles.set(true);
- this.codingJobBackendService.getVariableBundles(workspaceId).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
- next: variableBundles => {
- this.availableVariableBundles.set(variableBundles
- .filter(variableBundle => variableBundle.id !== undefined)
- .sort((a, b) => a.name.localeCompare(b.name)));
- this.refreshJobDefinitionOptions();
- this.isLoadingVariableBundles.set(false);
- this.applyQuickFilterUnitSelection();
- },
- error: () => {
- this.availableVariableBundles.set([]);
- this.refreshJobDefinitionOptions();
- this.isLoadingVariableBundles.set(false);
- }
- });
+ this.codingJobBackendService
+ .getVariableBundles(workspaceId)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe({
+ next: variableBundles => {
+ this.changeDetectorRef.markForCheck();
+ this.availableVariableBundles.set(
+ variableBundles
+ .filter(variableBundle => variableBundle.id !== undefined)
+ .sort((a, b) => a.name.localeCompare(b.name))
+ );
+ this.refreshJobDefinitionOptions();
+ this.isLoadingVariableBundles.set(false);
+ this.applyQuickFilterUnitSelection();
+ },
+ error: () => {
+ this.changeDetectorRef.markForCheck();
+ this.availableVariableBundles.set([]);
+ this.refreshJobDefinitionOptions();
+ this.isLoadingVariableBundles.set(false);
+ }
+ });
}
private applyQuickFilterUnitSelection(): void {
if (this.availableUnits().length === 0) {
return;
}
-
const filterVariableSelections: Map>[] = [];
const selectedJobDefinition = this.getSelectedJobDefinition();
if (selectedJobDefinition) {
filterVariableSelections.push(this.getJobDefinitionVariableSelections(selectedJobDefinition));
}
-
if (this.selectedVariableBundleIds().length > 0) {
filterVariableSelections.push(this.getSelectedVariableBundleVariableSelections());
}
-
if (filterVariableSelections.length === 0) {
this.unitList.set([]);
return;
}
-
const variableSelections = this.intersectVariableSelectionMaps(filterVariableSelections);
if (!this.hasVariableSelections(variableSelections)) {
this.unitList.set([]);
return;
}
-
const unitKeys = new Set(variableSelections.keys());
- this.unitList.set(this.availableUnits()
- .filter(unit => (
- unitKeys.has(this.normalizeUnitKey(unit.unitKey)) ||
- unitKeys.has(this.normalizeUnitKey(unit.unitName))
- ))
- .map(unit => unit.unitId));
+ this.unitList.set(
+ this.availableUnits()
+ .filter(
+ unit => unitKeys.has(this.normalizeUnitKey(unit.unitKey)) || unitKeys.has(this.normalizeUnitKey(unit.unitName))
+ )
+ .map(unit => unit.unitId)
+ );
}
private getSelectedJobDefinition(): JobDefinition | undefined {
if (this.selectedJobDefinitionId() === null) {
return undefined;
}
-
- return this.availableJobDefinitions().find(
- jobDefinition => jobDefinition.id === this.selectedJobDefinitionId()
- );
+ return this.availableJobDefinitions().find(jobDefinition => jobDefinition.id === this.selectedJobDefinitionId());
}
private refreshJobDefinitionOptions(): void {
- this.jobDefinitionOptions.set(this.availableJobDefinitions()
- .map(jobDefinition => {
+ this.jobDefinitionOptions.set(
+ this.availableJobDefinitions().map(jobDefinition => {
const label = this.getJobDefinitionLabel(jobDefinition);
const meta = this.getJobDefinitionMeta(jobDefinition);
const bundleSummary = this.getJobDefinitionBundleSummary(jobDefinition);
@@ -458,16 +495,16 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
bundleSummary,
summary
};
- }));
+ })
+ );
this.updateSelectedJobDefinitionDisplay();
}
private updateSelectedJobDefinitionDisplay(): void {
- const selectedOption = this.jobDefinitionOptions().find(
- option => option.id === this.selectedJobDefinitionId()
+ const selectedOption = this.jobDefinitionOptions().find(option => option.id === this.selectedJobDefinitionId());
+ this.selectedJobDefinitionLabel.set(
+ selectedOption?.label || this.translateService.instant('coding.job-definition-all')
);
- this.selectedJobDefinitionLabel.set(selectedOption?.label ||
- this.translateService.instant('coding.job-definition-all'));
this.selectedJobDefinitionSummary.set(selectedOption?.summary || '');
}
@@ -475,35 +512,25 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
if (this.selectedVariableBundleIds().length === 0) {
return [];
}
-
- return this.availableVariableBundles().filter(variableBundle => (
- this.selectedVariableBundleIds().includes(variableBundle.id)
- ));
+ return this.availableVariableBundles().filter(variableBundle => this.selectedVariableBundleIds().includes(variableBundle.id)
+ );
}
private getSelectedVariableBundleVariableSelections(): Map> {
const variableSelections = new Map>();
- this.getSelectedVariableBundles()
- .forEach(variableBundle => this.addVariableSelections(
- variableSelections,
- variableBundle.variables
- ));
+ this.getSelectedVariableBundles().forEach(variableBundle => this.addVariableSelections(variableSelections, variableBundle.variables)
+ );
return variableSelections;
}
- private intersectVariableSelectionMaps(
- selectionMaps: Map>[]
- ): Map> {
+ private intersectVariableSelectionMaps(selectionMaps: Map>[]): Map> {
const [firstSelectionMap, ...otherSelectionMaps] = selectionMaps;
if (!firstSelectionMap) {
return new Map>();
}
-
const intersection = new Map(
- [...firstSelectionMap.entries()]
- .map(([unitKey, variableIds]) => [unitKey, new Set(variableIds)])
+ [...firstSelectionMap.entries()].map(([unitKey, variableIds]) => [unitKey, new Set(variableIds)])
);
-
otherSelectionMaps.forEach(selectionMap => {
[...intersection.entries()].forEach(([unitKey, variableIds]) => {
const nextVariableIds = selectionMap.get(unitKey);
@@ -511,7 +538,6 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
intersection.delete(unitKey);
return;
}
-
const intersectedVariableIds = new Set(
[...variableIds].filter(variableId => nextVariableIds.has(variableId))
);
@@ -519,28 +545,19 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
intersection.delete(unitKey);
return;
}
-
intersection.set(unitKey, intersectedVariableIds);
});
});
-
return intersection;
}
- private getJobDefinitionVariableSelections(
- jobDefinition: JobDefinition
- ): Map> {
+ private getJobDefinitionVariableSelections(jobDefinition: JobDefinition): Map> {
const variableSelections = new Map>();
this.addVariableSelections(variableSelections, jobDefinition.assignedVariables || []);
- (jobDefinition.assignedVariableBundles || [])
- .forEach(bundle => {
- const hydratedBundle = this.getHydratedVariableBundle(bundle.id);
- this.addVariableSelections(
- variableSelections,
- hydratedBundle?.variables || []
- );
- });
-
+ (jobDefinition.assignedVariableBundles || []).forEach(bundle => {
+ const hydratedBundle = this.getHydratedVariableBundle(bundle.id);
+ this.addVariableSelections(variableSelections, hydratedBundle?.variables || []);
+ });
return variableSelections;
}
@@ -548,17 +565,17 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
if (bundleId === undefined) {
return undefined;
}
-
- return this.availableVariableBundles().find(
- availableBundle => availableBundle.id === bundleId
- );
+ return this.availableVariableBundles().find(availableBundle => availableBundle.id === bundleId);
}
- private getJobDefinitionVariables(jobDefinition: JobDefinition): Array<{ unitName?: string; variableId?: string }> {
+ private getJobDefinitionVariables(jobDefinition: JobDefinition): Array<{
+ unitName?: string;
+ variableId?: string;
+ }> {
const directVariables = jobDefinition.assignedVariables || [];
- const bundleVariables = (jobDefinition.assignedVariableBundles || [])
- .flatMap(bundle => this.getHydratedVariableBundle(bundle.id)?.variables || bundle.variables || []);
-
+ const bundleVariables = (jobDefinition.assignedVariableBundles || []).flatMap(
+ bundle => this.getHydratedVariableBundle(bundle.id)?.variables || bundle.variables || []
+ );
return [...directVariables, ...bundleVariables];
}
@@ -568,7 +585,6 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
.map(variable => `${this.normalizeUnitKey(variable.unitName)}:${variable.variableId || ''}`)
.filter(variableKey => variableKey !== ':')
);
-
return variableKeys.size;
}
@@ -578,7 +594,6 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
.map(variable => this.normalizeUnitKey(variable.unitName))
.filter(Boolean)
);
-
return unitKeys.size;
}
@@ -598,27 +613,24 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
private addVariableSelections(
variableSelections: Map>,
- variables: Array<{ unitName?: string; variableId?: string }>
+ variables: Array<{
+ unitName?: string;
+ variableId?: string;
+ }>
): void {
variables.forEach(variable => {
const unitKey = this.normalizeUnitKey(variable.unitName);
- const variableId = this.normalizeVariableIdForUnit(
- unitKey,
- variable.variableId
- );
+ const variableId = this.normalizeVariableIdForUnit(unitKey, variable.variableId);
if (!unitKey || !variableId) {
return;
}
-
const variableIds = variableSelections.get(unitKey) ?? new Set();
variableIds.add(variableId);
variableSelections.set(unitKey, variableIds);
});
}
- private hasVariableSelections(
- variableSelections: Map>
- ): boolean {
+ private hasVariableSelections(variableSelections: Map>): boolean {
return [...variableSelections.values()].some(variableIds => variableIds.size > 0);
}
@@ -629,42 +641,37 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
.toUpperCase();
}
- private normalizeVariableIdForUnit(
- unitKey: string,
- value: string | undefined
- ): string {
+ private normalizeVariableIdForUnit(unitKey: string, value: string | undefined): string {
const variableId = (value || '').trim();
if (!unitKey || !variableId) {
return '';
}
-
- const unit = this.availableUnits().find(availableUnit => (
- this.normalizeUnitKey(availableUnit.unitKey) === unitKey ||
- this.normalizeUnitKey(availableUnit.unitName) === unitKey
- ));
+ const unit = this.availableUnits().find(
+ availableUnit => this.normalizeUnitKey(availableUnit.unitKey) === unitKey ||
+ this.normalizeUnitKey(availableUnit.unitName) === unitKey
+ );
if (!unit?.unitData) {
return variableId;
}
-
try {
const scheme = JSON.parse(unit.unitData) as {
- variableCodings?: Array<{ id?: string; alias?: string }>;
+ variableCodings?: Array<{
+ id?: string;
+ alias?: string;
+ }>;
};
const variableCodings = scheme.variableCodings || [];
- const matchingVariableCoding = variableCodings
- .find(variableCoding => variableCoding.alias?.trim() === variableId) ||
- variableCodings
- .find(variableCoding => variableCoding.id?.trim() === variableId);
-
- return matchingVariableCoding?.alias?.trim() ||
- matchingVariableCoding?.id?.trim() ||
- variableId;
+ const matchingVariableCoding =
+ variableCodings.find(variableCoding => variableCoding.alias?.trim() === variableId) ||
+ variableCodings.find(variableCoding => variableCoding.id?.trim() === variableId);
+ return matchingVariableCoding?.alias?.trim() || matchingVariableCoding?.id?.trim() || variableId;
} catch {
return variableId;
}
}
private handleValidationResults(results: ValidateCodingCompletenessResponseDto | null): void {
+ this.changeDetectorRef.markForCheck();
this.validationResults.set(results);
if (results) {
this.validationCacheKey.set(results.cacheKey || null);
@@ -672,26 +679,43 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
}
private handleValidationProgress(progress: ValidationProgress): void {
+ this.changeDetectorRef.markForCheck();
this.validationProgress.set(progress);
+ this.isValidating.set(progress.status === 'loading' || progress.status === 'processing');
}
private loadMissingsProfiles(): void {
const workspaceId = this.appService.selectedWorkspaceId;
if (workspaceId) {
- this.missingsProfileService.getMissingsProfiles(workspaceId).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
- next: profiles => {
- this.missingsProfiles.set([{ id: 0, label: '' }, ...profiles.map((profile: { label: string; id: number }) => ({ id: profile.id ?? 0, label: profile.label }))]);
- this.selectedMissingsProfile.set(0);
- this.updateContentOption('missingsProfile', this.selectedMissingsProfile().toString());
- },
- error: () => {
- // Error occurred while loading missings profiles
- }
- });
+ this.missingsProfileService
+ .getMissingsProfiles(workspaceId)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe({
+ next: profiles => {
+ this.changeDetectorRef.markForCheck();
+ this.missingsProfiles.set([
+ { id: 0, label: '' },
+ ...profiles.map((profile: { label: string; id: number }) => ({
+ id: profile.id ?? 0,
+ label: profile.label
+ }))
+ ]);
+ this.selectedMissingsProfile.set(0);
+ this.contentOptions.update(current => ({
+ ...current,
+ missingsProfile: this.selectedMissingsProfile().toString()
+ }));
+ },
+ error: () => {
+ this.changeDetectorRef.markForCheck();
+ // Error occurred while loading missings profiles
+ }
+ });
}
}
exportCodingBook(): void {
+ if (this.isExportBusy) return;
const workspaceId = this.appService.selectedWorkspaceId;
if (!workspaceId) {
return;
@@ -699,53 +723,52 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
if (this.unitList().length === 0) {
return;
}
-
- this.updateContentOption('missingsProfile', this.selectedMissingsProfile().toString());
- this.updateContentOption('jobDefinitionId', this.selectedJobDefinitionId());
- this.updateContentOption('variableBundleIds', this.selectedVariableBundleIds());
+ this.contentOptions.update(current => ({
+ ...current,
+ missingsProfile: this.selectedMissingsProfile().toString()
+ }));
+ this.contentOptions.update(current => ({ ...current, jobDefinitionId: this.selectedJobDefinitionId() }));
+ this.contentOptions.update(current => ({ ...current, variableBundleIds: this.selectedVariableBundleIds() }));
this.codebookJobStatus.set('pending');
this.codebookJobProgress.set(0);
this.codebookJobError.set(null);
this.codebookJobId.set(null);
-
- this.exportService.startCodebookJob(
- workspaceId,
- this.contentOptions().missingsProfile,
- this.contentOptions(),
- this.unitList()
- ).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
- next: response => {
- this.codebookJobId.set(response.jobId);
- this.startCodebookPolling(workspaceId, response.jobId);
- },
- error: () => {
- this.codebookJobStatus.set('failed');
- this.codebookJobError.set('Failed to start codebook generation job');
- }
- });
+ this.exportService
+ .startCodebookJob(workspaceId, this.contentOptions().missingsProfile, this.contentOptions(), this.unitList())
+ .pipe(takeUntil(this.destroy$))
+ .subscribe({
+ next: response => {
+ this.changeDetectorRef.markForCheck();
+ this.codebookJobId.set(response.jobId);
+ this.startCodebookPolling(workspaceId, response.jobId);
+ },
+ error: () => {
+ this.changeDetectorRef.markForCheck();
+ this.codebookJobStatus.set('failed');
+ this.codebookJobError.set('Failed to start codebook generation job');
+ }
+ });
}
private startCodebookPolling(workspaceId: number, jobId: string): void {
this.stopCodebookPolling();
-
this.codebookPollingSubscription = interval(1500)
.pipe(
- takeUntil(this.destroy$),
- switchMap(() => this.exportService.getCodebookJobStatus(workspaceId, jobId))
+ exhaustMap(() => this.exportService.getCodebookJobStatus(workspaceId, jobId)),
+ takeUntil(this.destroy$)
)
.subscribe({
next: status => {
+ this.changeDetectorRef.markForCheck();
if (!status.status && status.error) {
this.codebookJobStatus.set('failed');
this.codebookJobError.set(status.error);
this.stopCodebookPolling();
return;
}
-
this.codebookJobProgress.set(status.progress || 0);
-
if (status.status === 'completed') {
- this.codebookJobStatus.set('completed');
+ this.codebookJobStatus.set('processing');
this.stopCodebookPolling();
this.downloadCodebookResult(workspaceId, jobId);
} else if (status.status === 'failed') {
@@ -759,6 +782,7 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
}
},
error: () => {
+ this.changeDetectorRef.markForCheck();
this.codebookJobStatus.set('failed');
this.codebookJobError.set('Failed to get job status');
this.stopCodebookPolling();
@@ -774,24 +798,30 @@ export class ExportCodingBookComponent implements OnInit, OnDestroy {
}
private downloadCodebookResult(workspaceId: number, jobId: string): void {
- this.exportService.downloadCodebookFile(workspaceId, jobId).pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
- next: blob => {
- const url = window.URL.createObjectURL(blob);
- const a = document.createElement('a');
- const timestamp = this.datePipe.transform(new Date(), 'yyyyMMdd_HHmmss');
- const fileExtension = this.contentOptions().exportFormat.toLowerCase();
- a.href = url;
- a.download = `codebook_${timestamp}.${fileExtension}`;
- document.body.appendChild(a);
- a.click();
- window.URL.revokeObjectURL(url);
- document.body.removeChild(a);
- },
- error: () => {
- this.codebookJobStatus.set('failed');
- this.codebookJobError.set('Failed to download codebook file');
- }
- });
+ this.exportService
+ .downloadCodebookFile(workspaceId, jobId)
+ .pipe(takeUntil(this.destroy$))
+ .subscribe({
+ next: blob => {
+ this.changeDetectorRef.markForCheck();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ const timestamp = this.datePipe.transform(new Date(), 'yyyyMMdd_HHmmss');
+ const fileExtension = this.contentOptions().exportFormat.toLowerCase();
+ a.href = url;
+ a.download = `codebook_${timestamp}.${fileExtension}`;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+ this.codebookJobStatus.set('completed');
+ },
+ error: () => {
+ this.changeDetectorRef.markForCheck();
+ this.codebookJobStatus.set('failed');
+ this.codebookJobError.set('Failed to download codebook file');
+ }
+ });
}
resetCodebookJob(): void {
diff --git a/apps/frontend/src/app/coding/components/export-coding-book/export-coding-book.component.zoneless.spec.ts b/apps/frontend/src/app/coding/components/export-coding-book/export-coding-book.component.zoneless.spec.ts
index 3c8149cf9..21e7a2e79 100644
--- a/apps/frontend/src/app/coding/components/export-coding-book/export-coding-book.component.zoneless.spec.ts
+++ b/apps/frontend/src/app/coding/components/export-coding-book/export-coding-book.component.zoneless.spec.ts
@@ -1,14 +1,130 @@
import { provideZonelessChangeDetection } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { By } from '@angular/platform-browser';
+import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { TranslateModule } from '@ngx-translate/core';
+import { CodebookExportComponent } from '@iqb/ngx-coding-components/codebook-export';
import { BehaviorSubject, of, Subject } from 'rxjs';
import { ExportCodingBookComponent } from './export-coding-book.component';
import { CodingExportService } from '../../services/coding-export.service';
import { CodingJobBackendService } from '../../services/coding-job-backend.service';
import { MissingsProfileService } from '../../services/missings-profile.service';
-import { ValidationStateService } from '../../services/validation-state.service';
import { FileService } from '../../../shared/services/file/file.service';
import { AppService } from '../../../core/services/app.service';
+import { ValidationStateService } from '../../services/validation-state.service';
+
+describe('shared codebook with zoneless change detection', () => {
+ let fixture: ComponentFixture;
+ let units: Subject<{ id: number; unitId: string; fileName: string; data: string }[]>;
+ let status: Subject<{ status: string; progress: number }>;
+ let download: Subject;
+ const start = jest.fn();
+ const getStatus = jest.fn();
+ const downloadFile = jest.fn();
+
+ beforeEach(async () => {
+ jest.useFakeTimers({ doNotFake: ['queueMicrotask', 'requestAnimationFrame'] });
+ units = new Subject();
+ status = new Subject();
+ download = new Subject();
+ start.mockReset().mockReturnValue(of({ jobId: 'job-1' }));
+ getStatus.mockReset().mockReturnValue(status);
+ downloadFile.mockReset().mockReturnValue(download);
+ await TestBed.configureTestingModule({
+ imports: [ExportCodingBookComponent, TranslateModule.forRoot()],
+ providers: [
+ provideZonelessChangeDetection(),
+ { provide: MatDialogRef, useValue: { close: jest.fn() } },
+ { provide: MatDialog, useValue: { open: jest.fn() } },
+ { provide: AppService, useValue: { selectedWorkspaceId: 1 } },
+ { provide: FileService, useValue: { getUnitsWithFileIds: () => units } },
+ {
+ provide: CodingJobBackendService,
+ useValue: { getJobDefinitions: () => of([]), getVariableBundles: () => of([]) }
+ },
+ { provide: MissingsProfileService, useValue: { getMissingsProfiles: () => of([]) } },
+ {
+ provide: CodingExportService,
+ useValue: { startCodebookJob: start, getCodebookJobStatus: getStatus, downloadCodebookFile: downloadFile }
+ },
+ {
+ provide: ValidationStateService,
+ useValue: {
+ validationProgress$: of({ status: 'idle', progress: 0, message: '' }),
+ validationResults$: of(null),
+ getValidationProgress: () => ({ status: 'idle', progress: 0, message: '' }),
+ getValidationResults: () => null
+ }
+ }
+ ]
+ }).compileComponents();
+ fixture = TestBed.createComponent(ExportCodingBookComponent);
+ });
+
+ afterEach(() => {
+ fixture?.destroy();
+ jest.useRealTimers();
+ });
+
+ async function loadUnits(): Promise {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ units.next([{
+ id: 1, unitId: 'UNIT', fileName: 'Aufgabe.vocs', data: '{}'
+ }]);
+ await fixture.whenStable();
+ return fixture.debugElement.query(By.directive(CodebookExportComponent)).componentInstance as CodebookExportComponent;
+ }
+
+ it('renders asynchronously loaded tasks and the common training filter without a manual refresh', async () => {
+ const shared = await loadUnits();
+ expect(fixture.nativeElement.textContent).toContain('Aufgabe');
+ expect(shared.loading).toBe(false);
+ expect(shared.showGroupColumn).toBe(false);
+ expect(fixture.nativeElement.querySelector('.codebook-training-filter')).not.toBeNull();
+ expect(fixture.nativeElement.textContent).not.toContain('coding.variable-bundle-filter');
+ expect(fixture.nativeElement.querySelector('mat-select[multiple]')).toBeNull();
+ expect(fixture.nativeElement.querySelector('.mat-column-group')).toBeNull();
+ });
+
+ it('renders delayed progress and download errors and keeps exports locked until download ends', async () => {
+ const shared = await loadUnits();
+ shared.toggleAll();
+ shared.exportCodingBook();
+ await fixture.whenStable();
+ expect(shared.busy).toBe(true);
+ jest.advanceTimersByTime(1500);
+ status.next({ status: 'processing', progress: 37 });
+ await fixture.whenStable();
+ expect(fixture.nativeElement.querySelector('.progress-percentage').textContent).toContain('37%');
+ status.next({ status: 'completed', progress: 100 });
+ await fixture.whenStable();
+ expect(shared.busy).toBe(true);
+ shared.exportCodingBook();
+ expect(start).toHaveBeenCalledTimes(1);
+ download.error(new Error('download failed'));
+ await fixture.whenStable();
+ expect(fixture.nativeElement.querySelector('.failed').textContent).toContain('Failed to download');
+ expect(shared.busy).toBe(false);
+ });
+
+ it('renders loading errors and stops outstanding status requests when destroyed', async () => {
+ fixture.detectChanges();
+ await fixture.whenStable();
+ units.error(new Error('load failed'));
+ await fixture.whenStable();
+ const shared = fixture.debugElement.query(By.directive(CodebookExportComponent)).componentInstance as CodebookExportComponent;
+ expect(shared.loading).toBe(false);
+ fixture.componentInstance.unitList.set([1]);
+ fixture.componentInstance.exportCodingBook();
+ jest.advanceTimersByTime(1500);
+ expect(status.observed).toBe(true);
+ fixture.destroy();
+ expect(status.observed).toBe(false);
+ jest.advanceTimersByTime(3000);
+ expect(getStatus).toHaveBeenCalledTimes(1);
+ });
+});
type ObservableValue = T extends import('rxjs').Observable ? V : never;
type JobStatus = ObservableValue>;
@@ -35,6 +151,8 @@ describe('Codebook export with delayed responses without Zone', () => {
imports: [ExportCodingBookComponent, TranslateModule.forRoot()],
providers: [
provideZonelessChangeDetection(),
+ { provide: MatDialogRef, useValue: { close: jest.fn() } },
+ { provide: MatDialog, useValue: { open: jest.fn() } },
{
provide: CodingExportService,
useValue: {
@@ -77,8 +195,8 @@ describe('Codebook export with delayed responses without Zone', () => {
id: 1, unitId: 'UNIT', fileName: 'UNIT.vocs', data: '{}'
}]);
await fixture.whenStable();
- const selectAll: HTMLInputElement = fixture.nativeElement.querySelector('.select-all-container input');
- selectAll.click();
+ const shared = fixture.debugElement.query(By.directive(CodebookExportComponent)).componentInstance as CodebookExportComponent;
+ shared.toggleAll();
await fixture.whenStable();
expect(exportButton().disabled).toBe(false);
exportButton().click();
@@ -89,20 +207,21 @@ describe('Codebook export with delayed responses without Zone', () => {
}
it('ends loading and displays selectable units after a delayed response', async () => {
- expect(fixture.nativeElement.querySelector('mat-spinner')).not.toBeNull();
+ expect(fixture.nativeElement.querySelector('mat-header-cell input').disabled).toBe(true);
units.next([{
id: 1, unitId: 'UNIT', fileName: 'UNIT.vocs', data: '{}'
}]);
await fixture.whenStable();
- expect(fixture.nativeElement.querySelector('mat-spinner')).toBeNull();
- expect(fixture.nativeElement.querySelector('table').textContent).toContain('UNIT');
- expect(fixture.nativeElement.querySelector('.selection-count').textContent).toContain('0 / 1');
+ expect(fixture.debugElement.query(By.directive(CodebookExportComponent)).componentInstance.loading).toBe(false);
+ expect(fixture.nativeElement.querySelector('mat-table').textContent).toContain('UNIT');
+ expect(fixture.nativeElement.querySelectorAll('mat-row')).toHaveLength(1);
+ expect(fixture.nativeElement.querySelector('mat-header-cell input').disabled).toBe(false);
});
it('ends loading after a delayed unit-list failure', async () => {
units.error(new Error('Units failed'));
await fixture.whenStable();
- expect(fixture.nativeElement.querySelector('mat-spinner')).toBeNull();
+ expect(fixture.debugElement.query(By.directive(CodebookExportComponent)).componentInstance.loading).toBe(false);
expect(exportButton().disabled).toBe(true);
});
@@ -117,9 +236,15 @@ describe('Codebook export with delayed responses without Zone', () => {
jest.advanceTimersByTime(1500);
status.next({ status: 'completed', progress: 100 });
await fixture.whenStable();
- expect(fixture.nativeElement.querySelector('.progress-container.completed')).not.toBeNull();
- expect(exportButton().disabled).toBe(false);
+ expect(fixture.nativeElement.querySelector('.progress-container.completed')).toBeNull();
+ expect(exportButton().disabled).toBe(true);
expect(downloadCodebookFile).toHaveBeenCalledWith(1, 'job-1');
+ window.URL.createObjectURL = jest.fn(() => 'blob:codebook');
+ window.URL.revokeObjectURL = jest.fn();
+ jest.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {});
+ download.next(new Blob(['codebook']));
+ await fixture.whenStable();
+ expect(exportButton().disabled).toBe(false);
});
it('renders delayed polling errors and resets through the bound button', async () => {
@@ -139,8 +264,8 @@ describe('Codebook export with delayed responses without Zone', () => {
id: 1, unitId: 'UNIT', fileName: 'UNIT.vocs', data: '{}'
}]);
await fixture.whenStable();
- const selectAll: HTMLInputElement = fixture.nativeElement.querySelector('.select-all-container input');
- selectAll.click();
+ const shared = fixture.debugElement.query(By.directive(CodebookExportComponent)).componentInstance as CodebookExportComponent;
+ shared.toggleAll();
await fixture.whenStable();
exportButton().click();
await fixture.whenStable();
diff --git a/apps/frontend/src/app/core/services/translation-loader.spec.ts b/apps/frontend/src/app/core/services/translation-loader.spec.ts
new file mode 100644
index 000000000..b20fd867c
--- /dev/null
+++ b/apps/frontend/src/app/core/services/translation-loader.spec.ts
@@ -0,0 +1,57 @@
+import { TestBed } from '@angular/core/testing';
+import { HttpClient, provideHttpClient } from '@angular/common/http';
+import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
+import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
+import { firstValueFrom } from 'rxjs';
+import applicationTranslations from '../../../assets/i18n/de.json';
+import { CacheBustingTranslateLoader } from './translation-loader';
+
+describe('shared component translations in Kodierbox', () => {
+ let translate: TranslateService;
+ let http: HttpTestingController;
+
+ beforeEach(() => {
+ TestBed.configureTestingModule({
+ imports: [TranslateModule.forRoot({
+ loader: {
+ provide: TranslateLoader,
+ useFactory: (client: HttpClient) => new CacheBustingTranslateLoader(client),
+ deps: [HttpClient]
+ }
+ })],
+ providers: [provideHttpClient(), provideHttpClientTesting()]
+ });
+ translate = TestBed.inject(TranslateService);
+ http = TestBed.inject(HttpTestingController);
+ });
+
+ afterEach(() => http.verify());
+
+ it('uses component defaults without copying Codebook labels into the application file', async () => {
+ expect(applicationTranslations).not.toHaveProperty('codebook');
+ const loaded = firstValueFrom(translate.use('de'));
+ http.expectOne(request => /^\.\/assets\/i18n\/de\.json\?v=\d+$/.test(request.url))
+ .flush(applicationTranslations);
+ await loaded;
+ expect(translate.instant('codebook.export')).toBe('Exportieren');
+ expect(translate.instant('codebook.dialogs.cancel')).toBe('Abbrechen');
+ expect(translate.instant('codebook.coding.selected-units', { count: 2 })).toBe('2 Aufgaben ausgewählt');
+ });
+
+ it('preserves application-only labels and deliberate overrides', async () => {
+ const loaded = firstValueFrom(translate.use('de'));
+ http.expectOne(request => request.url.startsWith('./assets/i18n/de.json?'))
+ .flush({ applicationOnly: 'Eigener Text', codebook: { export: 'Herunterladen' } });
+ await loaded;
+ expect(translate.instant('applicationOnly')).toBe('Eigener Text');
+ expect(translate.instant('codebook.export')).toBe('Herunterladen');
+ expect(translate.instant('codebook.dialogs.cancel')).toBe('Abbrechen');
+ });
+
+ it('selects the German component defaults for a regional locale', async () => {
+ const loaded = firstValueFrom(translate.use('de-AT'));
+ http.expectOne(request => request.url.startsWith('./assets/i18n/de-AT.json?')).flush({});
+ await loaded;
+ expect(translate.instant('codebook.export')).toBe('Exportieren');
+ });
+});
diff --git a/apps/frontend/src/app/core/services/translation-loader.ts b/apps/frontend/src/app/core/services/translation-loader.ts
new file mode 100644
index 000000000..783554844
--- /dev/null
+++ b/apps/frontend/src/app/core/services/translation-loader.ts
@@ -0,0 +1,24 @@
+import { HttpClient } from '@angular/common/http';
+import { TranslateLoader } from '@ngx-translate/core';
+import { Observable, map } from 'rxjs';
+import {
+ NGX_CODING_COMPONENTS_DE_TRANSLATIONS,
+ mergeTranslations,
+ selectTranslationsForLanguage,
+ TranslationMap
+} from '@iqb/ngx-coding-components/translations';
+
+const translationCacheBust = Date.now().toString();
+const componentTranslations = { de: NGX_CODING_COMPONENTS_DE_TRANSLATIONS };
+
+export class CacheBustingTranslateLoader implements TranslateLoader {
+ constructor(private http: HttpClient) {}
+
+ getTranslation(lang: string): Observable {
+ return this.http.get(`./assets/i18n/${lang}.json?v=${translationCacheBust}`)
+ .pipe(map(applicationTranslations => mergeTranslations(
+ selectTranslationsForLanguage(lang, componentTranslations),
+ applicationTranslations
+ )));
+ }
+}
diff --git a/cypress/zoneless/statistics-codebook.cy.ts b/cypress/zoneless/statistics-codebook.cy.ts
index 7316f04d0..a8bc5bbb5 100644
--- a/cypress/zoneless/statistics-codebook.cy.ts
+++ b/cypress/zoneless/statistics-codebook.cy.ts
@@ -213,12 +213,16 @@ describe('Zoneless replay statistics and codebook export', () => {
cy.window().then(win => { win.location.hash = '/workspace-admin/5/coding/management'; });
cy.get('coding-box-coding-management .action-buttons-toolbar button').contains('Codebook').click();
cy.get('coding-box-export-coding-book').as('dialog');
- cy.get('@dialog').find('mat-spinner').should('be.visible');
+ cy.get('@dialog').should('not.contain.text', 'codebook.');
+ cy.get('@dialog').find('button[type="submit"]').should('contain.text', 'Exportieren');
+ cy.get('@dialog').find('ngx-codebook-export mat-header-cell input').should('be.disabled');
cy.then(() => { releaseUnits(); });
cy.wait('@units');
- cy.get('@dialog').find('mat-spinner').should('not.exist');
- cy.get('@dialog').find('.select-all-container input').check();
- cy.get('@dialog').find('.options-grid mat-checkbox').eq(1).find('input').uncheck();
+ cy.get('@dialog').contains('button', 'Abbrechen').scrollIntoView().should('be.visible');
+ cy.get('@dialog').find('ngx-codebook-export mat-header-cell input').should('not.be.disabled');
+ cy.get('@dialog').find('ngx-codebook-export mat-header-cell input').check();
+ cy.get('@dialog').should('contain.text', '1 Aufgaben ausgewählt');
+ cy.get('@dialog').contains('mat-checkbox', 'Allgemeine Hinweise für jede Variable').find('input').uncheck();
cy.get('@dialog').find('button[type="submit"]').click();
cy.wait('@startCodebook');
cy.get('@dialog').find('button[type="submit"]').should('be.disabled');
diff --git a/docs/autocoder-solver-compatibility.md b/docs/autocoder-solver-compatibility.md
new file mode 100644
index 000000000..c785dddc6
--- /dev/null
+++ b/docs/autocoder-solver-compatibility.md
@@ -0,0 +1,62 @@
+# SOLVER-Kompatibilität der Kodierbox
+
+Kodierbox verwendet `@iqb/responses` 5.3.1. Validierung, Abhängigkeitsgraph und
+Codebook-Regeltexte verwenden weiterhin das unveränderte installierte Paket.
+Für automatische Kodierung erhält dessen Engine die bisherige SOLVER-Mathematik.
+
+## Produktionsweg
+
+`CodingProcessService.codeAutocoderResponses` übergibt seine kanonisierten Daten
+an `generated/autocoder-compatibility-engine.js`. Diese Datei wird aus dem
+installierten öffentlichen `coding-scheme-factory`-Einstieg von Responses 5.3.1
+mit esbuild erzeugt. Der Engine-Quellcode wird nicht manuell bearbeitet. Nur der
+mathjs-Import wird auf `autocoder-solver-math.ts` umgeleitet.
+
+Dieser Adapter verwendet eine private mathjs-12.4.3-Instanz mit der bisherigen
+relativen Toleranz 1e-12. Die exakte bisherige Math-Version erhält auch Funktionen
+jenseits von Vergleich und Rundung; nur die neue absolute Toleranz auf null zu
+setzen würde nicht alle Unterschiede zwischen mathjs 12 und 15 ausschließen.
+Die Konfiguration ist wie beim bisherigen Default-Einstieg schreibgeschützt,
+auch in verschachtelten evaluate/parser-Aufrufen. Globale mathjs-Instanzen und
+die normale Responses-Factory werden nicht verändert.
+
+Ein NULL-Ergebnis löst vor der Rückgabe der Auswertung einen Fehler aus. Der
+bestehende Fehlerpfad der 5.3.1-Engine ergibt dadurch wie bisher DERIVE_ERROR.
+Die Korrektur wirkt innerhalb des ursprünglichen Ableitungsablaufs: Codeauswahl
+und abhängige SUM_SCORE-/weitere Ableitungen sehen bereits den richtigen Status.
+Es werden keine fertigen Ergebnisse nachträglich umgeschrieben.
+
+## Nachweise
+
+Drei Service-Regressionen sichern ceil nahe null, Gleichheit nahe null und einen
+NULL-Zweig mit echten Regeln, Alias und Subform. Sie scheiterten vor dem Fix.
+Die unabhängige Vergleichsmatrix prüft 11.760 Paare gegen das veröffentlichte
+Responses 5.2.0: sieben Ableitungsarten, 14 Statuswerte, 14 Wertformen, Aliase,
+Subforms und neun SOLVER-Ausdrücke. Weitere Fälle prüfen bestehende Zielzustände,
+BASE_NO_VALUE, Ableitungsketten, nicht endliche Ergebnisse und Konfigurationsschutz.
+
+`@iqb/responses-legacy-reference` ist ausschließlich eine Entwicklungsabhängigkeit
+für diese unabhängige Referenz. Das erzeugte Produktionsbundle enthält Engine-Code
+aus Responses 5.3.1, keine 5.2.0-Engine. Die Runtime benötigt das Referenzpaket nicht.
+
+## Wartung
+
+Nach einem bewusst geprüften Responses- oder Adapter-Update:
+
+```sh
+node scripts/build-autocoder-compat.mjs
+node scripts/build-autocoder-compat.mjs --check
+npx nx test backend --runInBand
+npx nx build backend
+```
+
+Der Builder prüft die Responses-Version 5.3.1, mathjs 12.4.3 und genau einen
+umgeleiteten Math-Import. Der Backend-Test baut das Artefakt erneut und vergleicht
+es bytegenau mit der eingecheckten Datei. Änderungen an Paket, Adapter oder
+Generator können somit nicht unbemerkt ein veraltetes Bundle freigeben.
+Der generierte Header dokumentiert Herkunft und Lizenzen. Die Deklaration
+übernimmt den öffentlichen Factory-Vertrag direkt aus Responses.
+
+Frontend und Backend auf dem Entwicklungsrechner getrennt prüfen. Die gemeinsame
+Bibliothek, Studios Schemer/Preview und beide Codebook-Generatoren erhalten diese
+Kodierbox-spezifische Kompatibilität nicht; deren bisheriges Verhalten bleibt bestehen.
diff --git a/docs/codebook-acceptance-2026-10-13.md b/docs/codebook-acceptance-2026-10-13.md
new file mode 100644
index 000000000..347b0a92f
--- /dev/null
+++ b/docs/codebook-acceptance-2026-10-13.md
@@ -0,0 +1,11 @@
+# Kodierbox: 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).
+
+58 Backend- und 60 Frontendtests; echte Redis-Jobs/Downloads, Jobdefinitionspicker, Missingprofil, Score-Abwahl, Doppelauslösung, Fehleranzeige, Retry, Polling-Ende und Workspace-Grenzen bestanden.
+
+Beide Host-Builds und Backend-Lint erfolgreich. Identischer gepackter 4.1.0-Tarball in beiden Hosts; SHA256: `dcee6cc39436baf91cf2558afedfbc202e044ca14dbf72c7bfdf4b381e5c0ccd`. Der Installationsvergleich über die echten Adapter besteht für 4.654 JSON-/DOCX-Exportpaare.
+
+Microsoft Office Web Viewer / Word hat die tatsächlichen Browser-Downloads gerendert: alle drei vollständigen Seiten sind zwischen Studio und Kodierbox pixelgleich. Sechs Seiteninhalte mit/ohne Scores sind außerdem mit dem ursprünglichen Studio pixelgleich, abgesehen von der gesondert geprüften Fußzeile „IQB Codebook“. Die zusätzliche Editor-Matrix prüft 384 Formatkombinationen über ESM und CommonJS; alle 48 gültigen Markierungskombinationen wurden auch in Word gerendert. Insgesamt 1.206 Generator-Tests und 20 Office-Seiten. Desktop-Word und ein vollständiger Aspect-Editor-/Player-Lauf wurden nicht ausgeführt.
+
+Vollständige Bedingungen, Belege, CI-Grenzen und bestehende Layoutgrenzen: [gemeinsames Abnahmeprotokoll](https://github.com/iqb-berlin/coding-components/blob/shared-codebook/docs/codebook-acceptance-2026-10-13.md). Die vollständigen Host-Suiten wurden für diesen Fix nicht erneut ausgeführt. Lokale Abnahme ist kein Merge-/Release-/Deployment-Nachweis.
diff --git a/docs/codebook-alpha-2026-10-15.md b/docs/codebook-alpha-2026-10-15.md
new file mode 100644
index 000000000..b9cda313a
--- /dev/null
+++ b/docs/codebook-alpha-2026-10-15.md
@@ -0,0 +1,66 @@
+# Codebook-Alpha nach dem aktuellen develop-Abgleich
+
+> Historischer Bericht vom 15.10.2026. Der Branch verwendet inzwischen den
+> [korrigierten Entwicklungsbuild vom 17.10.2026](codebook-integration-2026-10-17.md).
+> Die folgenden Paketangaben bleiben als Nachweis des damaligen Stands erhalten.
+
+Dieser Stand verwendet den veröffentlichten Tarball
+`vendor/iqb-ngx-coding-components-4.1.0-alpha.1.tgz` aus dem
+[Alpha-Release](https://github.com/iqb-berlin/coding-components/releases/tag/Schemer_v2.9.0-alpha.1).
+SHA-256: `cb85078662e8fe4490aa90001e369db059571da73ec73a1457a7ffb89d49f296`.
+Studio und Kodierbox verwenden dieselbe Paketdatei; alle 20 installierten
+Paketdateien stimmen mit dem Release-Build überein. Responses bleibt 5.3.1.
+Die bestehende lokale SOLVER-Kompatibilität und der Antwortvertrag bleiben erhalten.
+
+## develop und Zoneless
+
+`develop` auf `31ad79f3d` wird übernommen, einschließlich Zoneless-Umstellung
+und verbindlicher Quality Gates. Die gemeinsame Codebook-Oberfläche bleibt
+bestehen; der Wrapper verwendet Signals und `inject()` wie die aktuelle Anwendung.
+Die manuellen Angular-Zone-Timer der ergänzten Tests sind durch Jest-Timer ersetzt.
+Die verzögerten Antwortfälle aus develop und die Prüfungen der gemeinsamen
+Oberfläche werden gemeinsam ausgeführt.
+
+Die Aufgaben-/Missingprofil-Auswahl und der Schulungsfilter bleiben gemeinsam.
+Jobdefinitionen und Jobstatus bleiben im Kodierbox-Wrapper; keine Gruppenspalte
+oder Variablenbündel-Auswahl im Dialog. Der serverseitige Scope-Vertrag bleibt
+bestehen. `exhaustMap` verhindert überlappende Statusabfragen; Schließen beendet
+laufende Abfragen und Downloads. Der Export bleibt bis zum Ende des Downloads
+gesperrt und wird bei Fehlern wieder freigegeben.
+
+Die von der gemeinsamen Bibliothek deklarierte Angular-Animations-Peerabhängigkeit
+wird ausdrücklich auf denselben Patchstand 21.2.24 wie Angular Core gesetzt.
+Das verhindert eine unpassende automatische Peerauflösung auf 21.2.25; es werden
+keine Zone- oder Animationsprovider in die Anwendung zurückgebracht.
+
+## Behobener CI-Audit-Befund
+
+Pipeline [104049](https://scm.cms.hu-berlin.de/iqb/coding-box/-/pipelines/104049)
+scheiterte im Job [audit-app / 474890](https://scm.cms.hu-berlin.de/iqb/coding-box/-/jobs/474890).
+Die 19 Paketkorrekturtests waren dort grün. Die beiden hohen npm-Befunde
+betrafen die verschachtelte `image-size@1.2.1`-Installation und die davon
+betroffene gemeinsame Bibliothek. Der Host erzwingt nun `image-size@2.0.3`.
+
+Der neue Backend-Test verwendet den tatsächlich vom installierten Generator
+aufgelösten Parser und einen begrenzten Kindprozess. Ein ICNS-Eintrag mit
+Eintragslänge 0 muss abgelehnt werden; ein gültiges PNG liefert weiter 1×1 Pixel.
+Der vollständige lokale npm-Audit meldet danach 0 hohe und 0 kritische Befunde;
+57 moderate und ein niedriger Befund bleiben sichtbar. Die Auditgrenze wird nicht
+abgeschwächt.
+
+Das Backend-Dockerfile kopiert denselben Alpha-Tarball und setzt die
+`image-size@2.0.3`-Override auch für seine separate Produktionsinstallation.
+
+## Installations- und Exportvergleich
+
+[Maschinenlesbarer Vergleich](codebook-alpha-installation-comparison-2026-10-15.json):
+4.694 Exportpaare durch die echten Studio-/Kodierbox-Adapter sind identisch,
+einschließlich JSON und aller verglichenen DOCX-Bestandteile. Unveränderte alte
+Studio-Referenzen, Leerzeichenfälle, Inlinebilder, Code/Missing 0 und gespeicherte
+Chemieformate sind enthalten. Beide Hosts verwenden dasselbe Alpha-Paket und
+dieselbe Responses-Auflösung im Host und in der Bibliothek.
+
+Persistenzgrenzen dieses Vergleichs liegen im Speicher. Der Vergleich ersetzt
+keinen aktuellen HTTP-/Browser-/Redis-/Word-Lauf. Historische Word-Abnahmen
+behalten ihre ursprünglichen Artefaktbezüge. Exakte lokale Tests und entfernte CI
+stehen im PR; ein Deployment ist damit nicht bestätigt.
diff --git a/docs/codebook-alpha-installation-comparison-2026-10-15.json b/docs/codebook-alpha-installation-comparison-2026-10-15.json
new file mode 100644
index 000000000..b27067534
--- /dev/null
+++ b/docs/codebook-alpha-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-v54DIARPSU9sO4prpUWWC4e4eUPafraIw31RCXuS1kU7e6QIHqXr4n51JgQSpa2SVUEM8lkmFoPWSZwcjrY+dQ==",
+ "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/codebook-aspect-acceptance-2026-10-13.md b/docs/codebook-aspect-acceptance-2026-10-13.md
new file mode 100644
index 000000000..3b6ff651e
--- /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/codebook-inline-images-2026-10-15.md b/docs/codebook-inline-images-2026-10-15.md
new file mode 100644
index 000000000..1fbb01b43
--- /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/codebook-installation-comparison-2026-10-17.json b/docs/codebook-installation-comparison-2026-10-17.json
new file mode 100644
index 000000000..d492f9b30
--- /dev/null
+++ b/docs/codebook-installation-comparison-2026-10-17.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-RjXrwSua+T89/XvhyqjmeOYnc/wUXpFMVr5aYGWeluDnHSnP8pTIJDme+mqDFgAJ1MQAIUbuQu7+rlPGhjkrrQ==",
+ "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/codebook-integration-2026-10-17.md b/docs/codebook-integration-2026-10-17.md
new file mode 100644
index 000000000..275c8eadf
--- /dev/null
+++ b/docs/codebook-integration-2026-10-17.md
@@ -0,0 +1,74 @@
+# Codebook-Integration vom 17.10.2026
+
+## Gemeinsames Paket
+
+Studio und Kodierbox installieren denselben Entwicklungsbuild:
+
+- Datei: `vendor/iqb-ngx-coding-components-4.1.0-dev.ff15d6d.tgz`
+- Quellcommit: `ff15d6d882417db26bc81fc88a09074eab74d57d` aus coding-components PR #251
+- SHA-256: `e67c04e07762350cb4ee3b1a70a36bcde0f2ba1c24df1f5a40000346d34fd112`
+- Integrität: `sha512-RjXrwSua+T89/XvhyqjmeOYnc/wUXpFMVr5aYGWeluDnHSnP8pTIJDme+mqDFgAJ1MQAIUbuQu7+rlPGhjkrrQ==`
+
+Der Build enthält die Checkbox-Korrektur, die DOCX-Korrektur für explizite
+Null-Einzüge sowie die gemeinsamen deutschen Codebook-Texte und Übersetzungshilfen.
+Die Quelle wurde mit `npm run build_cc` gebaut. Nur das erzeugte Paketmanifest
+bekam die Entwicklungsversionsnummer und `gitHead`, anschließend wurde genau
+einmal `npm pack` ausgeführt. Beide Hosts enthalten denselben Tarball.
+Versionsnummern im Bibliotheksquellcode und vorhandene Releases bleiben erhalten.
+Es wurde kein neuer Alpha-Kandidat, Git-Tag oder Registry-Release erstellt.
+
+Nach `git pull` installiert `npm ci` das eingecheckte Paket. Eine gesonderte
+manuelle Installation der Bibliothek ist nicht erforderlich. Das im Studio
+gewählte Schemer-HTML bleibt eine separat konfigurierte Anwendung im iframe;
+die Bibliotheksaktualisierung ersetzt dieses HTML nicht.
+
+## Übersetzungen
+
+Der Root-Loader führt die deutschen Bibliotheksvorgaben rekursiv mit der eigenen
+Sprachdatei zusammen. Eigene Begriffe überschreiben bei Bedarf einzelne Schlüssel.
+Die 42 kopierten `codebook.*`-Standardtexte sind aus der Anwendungsdatei entfernt;
+anwendungsspezifische Kodier-/Jobtexte bleiben erhalten. Alle Komponenten nutzen
+den bestehenden gemeinsamen TranslateService. Es wird kein zweiter Dienst erzeugt.
+`mergeTranslations` und `selectTranslationsForLanguage` kommen aus dem
+Bibliotheks-Einstieg `@iqb/ngx-coding-components/translations`.
+Cache-Busting für die Anwendungsdatei bleibt erhalten.
+
+## Paket- und Exportvergleich
+
+Alle 20 installierten Paketdateien stimmen mit dem gebauten Paket und miteinander
+überein. Die Tarball-Integrität stimmt mit beiden Root-Locks überein. Responses
+bleibt der gemeinsame Host-Peer 5.3.1. 4.694 Exportpaare über die echten
+Studio-/Kodierbox-Adapter ergeben identisches JSON und identische normalisierte
+DOCX-Bestandteile. Unveränderte alte Studio-Referenzen werden weiter verglichen;
+die Persistenzgrenzen dieses Vergleichs liegen im Speicher.
+Der maschinenlesbare Nachweis steht in
+[codebook-installation-comparison-2026-10-17.json](codebook-installation-comparison-2026-10-17.json).
+
+Kodierboxs bisheriger HTTP-Loader wird um die Bibliotheksvorgaben ergänzt.
+Das Backend-Dockerfile installiert denselben Tarball. Der bestehende
+`image-size@2.0.3`-Override und der SOLVER-Adapter bleiben erhalten.
+
+## Lokale Prüfung dieses Integrationsstands
+
+- Frontend: 2.865 Tests in 274 Suites bestanden, einschließlich neuer Tests mit
+ echter Anwendungs-Sprachdatei, Überschreibungen, Regionsfallback und Interpolation.
+- Backend: 2.764 Tests in 178 Suites bestanden; 17 Tests in drei vorhandenen Suites
+ bleiben übersprungen. Frontend und Backend wurden nacheinander validiert.
+- Frontend-/Backend-Lint und beide Produktionsbuilds bestanden.
+- Drei Chrome-Cypress-Tests am Produktionsfrontend bestanden, einschließlich
+ Codebook-Beschriftungen, interpolierter Auswahlanzahl, Fortschritt, erfolgreichem
+ Download und Fehleranzeige. Authentifizierung und HTTP-Antworten sind simuliert.
+- Separate Produktionsinstallation aus dem generierten Backend-Manifest, demselben
+ Tarball, allen generierten Overrides und dem gepinnten PostgreSQL-Treiber erfolgreich
+ mit npm 11.18.0 und `--omit=dev --ignore-scripts` (657 Pakete).
+- Alle 20 installierten Bibliotheksdateien stimmen auch in der Produktionsinstallation
+ mit dem gebauten Paket überein. JSON-/DOCX-Einstieg, Null-Einzug mit 100 Twips,
+ native Formel sowie ICNS-/PNG-Regression mit `image-size@2.0.3` bestanden.
+- Vollständiger Root-Audit: 0 hoch, 0 kritisch, 57 moderat, 1 niedrig.
+
+Der [Produktionspaket-Nachweis](codebook-production-check-2026-10-17.json)
+bezieht sich auf diese separate Installation. Native Installationsskripte und
+Docker-Start wurden dabei nicht ausgeführt. Ein Deployment und eine erneute
+Microsoft-Word-Abnahme des Originalfalls sind nicht bestätigt. Die entfernte CI
+ist am aktuellen PR-Head separat zu prüfen. Frühere Word-Abnahmen gelten für
+ihre dort dokumentierten Artefakte.
diff --git a/docs/codebook-production-check-2026-10-17.json b/docs/codebook-production-check-2026-10-17.json
new file mode 100644
index 000000000..9a55daa49
--- /dev/null
+++ b/docs/codebook-production-check-2026-10-17.json
@@ -0,0 +1,13 @@
+{
+ "package": "4.1.0-dev.ff15d6d",
+ "source": "ff15d6d882417db26bc81fc88a09074eab74d57d",
+ "responses": "5.3.1",
+ "imageSize": "2.0.3",
+ "icnsAndPng": true,
+ "json": true,
+ "docx": true,
+ "zeroIndentTwips": 100,
+ "nativeFormula": true,
+ "scriptsExecuted": false,
+ "dockerStarted": false
+}
diff --git a/docs/codebook-responses-2026-10-15.md b/docs/codebook-responses-2026-10-15.md
new file mode 100644
index 000000000..95349e466
--- /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-solver-compatibility-2026-10-15.md b/docs/codebook-solver-compatibility-2026-10-15.md
new file mode 100644
index 000000000..0e7a0145a
--- /dev/null
+++ b/docs/codebook-solver-compatibility-2026-10-15.md
@@ -0,0 +1,145 @@
+# 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.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;
+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.
+
+
+## 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
+
+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.md b/docs/codebook-word-windows-2026-10-15.md
new file mode 100644
index 000000000..6b0096c9b
--- /dev/null
+++ b/docs/codebook-word-windows-2026-10-15.md
@@ -0,0 +1,22 @@
+# Windows-Desktop-Word-Abnahme am 15.10.2026
+
+Der aktuelle gemeinsame Tarball mit SHA256
+`2df79a7b18196d7c25d7e02e15d5b148200d75ba2a315214ccfb8386536dd0a6`
+wurde in Microsoft Word LTSC 2024, Version 2408, Build 16.0.17932.21000 (64 Bit)
+auf Windows geprüft: **16 synthetische Dokumente mit 32 vollständig gesichteten
+Seiten bestehen**. Die sieben aktuellen Studio-/Kodierbox-Paare sind auf allen
+Seiten pixelgleich. Zwei unveränderte alte Studio-Rich-Text-Referenzen bleiben
+abgesehen von vereinbarter Kennzeichnung/Erstellungsdatum und der dokumentierten
+18pt-Schriftgrößenkorrektur erhalten. Alle DOCX-Quellprüfsummen sind unverändert.
+
+Die endgültige PowerShell-Prüfung wurde auf Windows erneut ausgeführt; alle
+32 Seiten stimmen pixelgenau mit dem visuell gesichteten Lauf überein.
+Es wurden in diesem Nachtrag ausschließlich Dokumentation und die gemeinsam
+verwalteten Prüfscripte geändert. Anwendungscode, installiertes Paket und
+unabhängige Studio-Referenzen bleiben unverändert.
+
+[Ausführlicher Bericht und Reproduktionsanleitung](https://github.com/iqb-berlin/coding-components/blob/shared-codebook/docs/codebook-word-windows-2026-10-15.md)
+und [maschinenlesbarer Nachweis mit Datei- und Seitenbild-Prüfsummen](https://github.com/iqb-berlin/coding-components/blob/shared-codebook/docs/codebook-word-windows-2026-10-15.json)
+liegen in coding-components. Der Nachweis gilt für die beschriebenen Fälle und
+diese Word-Version. Browser-/HTTP-/Redis-/Berechtigungsprüfungen und entfernte CI
+bleiben eigene Nachweise. Kein PR-Merge, Release oder Deployment.
diff --git a/docs/shared-codebook.md b/docs/shared-codebook.md
new file mode 100644
index 000000000..cae485a78
--- /dev/null
+++ b/docs/shared-codebook.md
@@ -0,0 +1,86 @@
+# Integration des gemeinsamen Codebooks
+
+## Aktueller Entwicklungsstand vom 17.10.2026
+
+Beide Anwendungen verwenden jetzt denselben korrigierten Entwicklungsbuild
+`4.1.0-dev.ff15d6d` aus Bibliothekscommit
+`ff15d6d882417db26bc81fc88a09074eab74d57d`.
+Der [Integrationsbericht](codebook-integration-2026-10-17.md) dokumentiert Paket,
+Übersetzungseinbindung und aktuelle Prüfungen. Es wurde kein neuer Alpha-Kandidat
+oder Registry-Release erstellt. Die folgenden Abnahmeberichte beziehen sich auf
+ihre jeweils genannten früheren Artefakte.
+
+## 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
+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.
+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
+
+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.
+
+Die Codebook-UI und der JSON-/DOCX-Generator kommen aus
+`@iqb/ngx-coding-components` 4.1.0. Studio bildet die Bedien- und Ausgabereferenz.
+Die vollständigen Regeln und Referenztests stehen in
+`coding-components/docs/shared-codebook.md`.
+
+Schulungsbedarf wird in beiden Anwendungen im gemeinsamen Formular ausgewählt
+und im gemeinsamen Generator anhand von CODER_TRAINING_REQUIRED gefiltert.
+Der Kodierbox-Wrapper ergänzt den Jobdefinitionsfilter. Die Gruppenspalte und
+der zusätzliche Variablenbündel-Filter werden hier nicht angezeigt (Issue #176).
+Der bestehende serverseitige Variablenbündel-Vertrag bleibt erhalten; Bündeldaten
+werden weiterhin zur korrekten Auflösung von Jobdefinitionen geladen.
+Workspace-Prüfung und serverseitige Schnittmengenbildung bleiben bestehen.
+Sowohl synchroner Export als auch Queue-Prozessor verwenden über den vorhandenen
+Generation-Service denselben gemeinsamen Generator. Jobstart, Statusabfrage,
+Fortschritt und Download bleiben ausschließlich in der Kodierbox. Schließen
+beendet die Statusabfrage, nicht den Serverjob.
+Der Wrapper meldet asynchrone Daten- und Statusänderungen an Angular und ist mit
+OnPush und zoneless Change Detection getestet.
+
+## Release-Kandidat
+
+Die Abhängigkeit zeigt vor Veröffentlichung auf
+`vendor/iqb-ngx-coding-components-4.1.0.tgz`. Das Backend-Dockerfile übernimmt
+dasselbe Artefakt ausdrücklich in die Runtime-Installation. Ohne diese Zuordnung
+würde Nx die Versionsnummer 4.1.0 im generierten Runtime-Manifest ausgeben und npm
+zu früh die noch unveröffentlichte Registry-Version installieren wollen.
+
+Nach gemeinsamer Abnahme: Bibliothek veröffentlichen, die Root-Abhängigkeit auf
+4.1.0 umstellen und Lock aktualisieren. Anschließend die Tarball-COPY- und
+`npm pkg set`-Schritte im Backend-Dockerfile sowie den Tarball entfernen und die
+Runtime-Installation erneut prüfen. Nicht nur die Frontend-Abhängigkeit ändern.
+
+Die Kodierengine verwendet wie Schemer, Validierung und Codebook-Regeltexte
+`@iqb/responses` 5.3.1. Die Bibliothek verwendet den Host-Peer. Der Anwendungs-
+und Persistenzvertrag bleibt bei response-Spezifikation 1.5.1; ein echter
+Engine-Regressionstest erhält auch den alten Status CODE_SELECTION_PENDING.
+Rollback erfolgt durch die vorherige Anwendungsversion, ohne Datenmigration.
diff --git a/package-lock.json b/package-lock.json
index a8030204a..c5105fc5d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.17.5",
"license": "MIT",
"dependencies": {
+ "@angular/animations": "21.2.24",
"@angular/cdk": "21.2.14",
"@angular/common": "21.2.24",
"@angular/compiler": "21.2.24",
@@ -21,7 +22,8 @@
"@angular/router": "21.2.24",
"@iqb/metadata-components": "1.0.0",
"@iqb/metadata-resolver": "0.2.0",
- "@iqb/responses": "^5.2.0",
+ "@iqb/ngx-coding-components": "file:vendor/iqb-ngx-coding-components-4.1.0-dev.ff15d6d.tgz",
+ "@iqb/responses": "5.3.1",
"@iqbspecs/coding-scheme": "^3.4.1",
"@iqbspecs/metadata-profile": "^0.11.1",
"@iqbspecs/metadata-values": "^3.0.1",
@@ -69,6 +71,7 @@
"keycloak-angular": "21.0.0",
"keycloak-js": "^26.2.4",
"libxmljs2": "^0.37.0",
+ "mathjs": "12.4.3",
"mathml2omml": "^0.5.0",
"multer": "2.4.0",
"passport-jwt": "^4.0.1",
@@ -96,6 +99,7 @@
"@angular/compiler-cli": "21.2.24",
"@golevelup/ts-jest": "^0.5.0",
"@iqb/eslint-config": "^2.1.1",
+ "@iqb/responses-legacy-reference": "npm:@iqb/responses@5.2.0",
"@nx/angular": "22.7.12",
"@nx/cypress": "22.7.12",
"@nx/esbuild": "22.7.12",
@@ -756,6 +760,22 @@
"typescript": "*"
}
},
+ "node_modules/@angular/animations": {
+ "version": "21.2.24",
+ "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.24.tgz",
+ "integrity": "sha512-l83bg0i6tmBSmbf2GhpccnJp827RDq7uDbZf3vjWPPCzrQUeFNbUjYgzINzoLAbjhZMxEz6Z2ALgouRCXYJ83w==",
+ "deprecated": "@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.",
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=24.0.0"
+ },
+ "peerDependencies": {
+ "@angular/core": "21.2.24"
+ }
+ },
"node_modules/@angular/build": {
"version": "21.2.24",
"resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.24.tgz",
@@ -3253,6 +3273,20 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@cortex-js/compute-engine": {
+ "version": "0.24.1",
+ "resolved": "https://registry.npmjs.org/@cortex-js/compute-engine/-/compute-engine-0.24.1.tgz",
+ "integrity": "sha512-B9pLaZFC1vBrL64yR2LQWy2sj1E/+wtwIVokPuPqX8YWXwj9W2AMVyS9pSMN8gEdjpAC50nSmtL6110Rgi7dKA==",
+ "license": "MIT",
+ "dependencies": {
+ "complex.js": "^2.1.1",
+ "decimal.js": "^10.4.3"
+ },
+ "engines": {
+ "node": ">=16.14.2",
+ "npm": ">=8.5.0"
+ }
+ },
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@@ -4534,6 +4568,19 @@
"eslint-config-airbnb-typescript": "^18.0.0"
}
},
+ "node_modules/@iqb/mathlive": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@iqb/mathlive/-/mathlive-0.4.1.tgz",
+ "integrity": "sha512-SjMXUBazoLvhDqpJVw6JvqD185B/XMKsINtcOIQSh8EU22ciMBpJbhFcDjOiD6DjPU65cFuOjmLMavp5jbp6iw==",
+ "license": "MIT",
+ "dependencies": {
+ "@cortex-js/compute-engine": "0.24.1"
+ },
+ "engines": {
+ "node": ">=16.14.2",
+ "npm": ">=8.5.0"
+ }
+ },
"node_modules/@iqb/metadata-components": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@iqb/metadata-components/-/metadata-components-1.0.0.tgz",
@@ -4566,15 +4613,223 @@
"@iqbspecs/metadata-profile": "^0.11.1"
}
},
+ "node_modules/@iqb/ngx-coding-components": {
+ "version": "4.1.0-dev.ff15d6d",
+ "resolved": "file:vendor/iqb-ngx-coding-components-4.1.0-dev.ff15d6d.tgz",
+ "integrity": "sha512-RjXrwSua+T89/XvhyqjmeOYnc/wUXpFMVr5aYGWeluDnHSnP8pTIJDme+mqDFgAJ1MQAIUbuQu7+rlPGhjkrrQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@iqb/mathlive": "^0.4.1",
+ "buffer": "6.0.3",
+ "cheerio": "1.1.2",
+ "docx": "8.5.0",
+ "image-size": "2.0.3",
+ "katex": "^0.16.11",
+ "mathml2omml": "0.5.0",
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/animations": "^21.0.0",
+ "@angular/cdk": "^21.0.0",
+ "@angular/common": "^21.0.0",
+ "@angular/compiler": "^21.0.0",
+ "@angular/core": "^21.0.0",
+ "@angular/elements": "^21.0.0",
+ "@angular/forms": "^21.0.0",
+ "@angular/material": "^21.0.0",
+ "@angular/platform-browser": "^21.0.0",
+ "@angular/platform-browser-dynamic": "^21.0.0",
+ "@angular/router": "^21.0.0",
+ "@iqb/responses": "^5.3.1",
+ "@iqbspecs/coding-scheme": "^3.4.1",
+ "@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",
+ "@tiptap/extension-blockquote": "^2.23.1",
+ "@tiptap/extension-bold": "^2.23.1",
+ "@tiptap/extension-bubble-menu": "^2.23.1",
+ "@tiptap/extension-bullet-list": "^2.23.1",
+ "@tiptap/extension-color": "^2.23.1",
+ "@tiptap/extension-document": "^2.23.1",
+ "@tiptap/extension-floating-menu": "^2.23.1",
+ "@tiptap/extension-font-family": "^2.23.1",
+ "@tiptap/extension-heading": "^2.23.1",
+ "@tiptap/extension-highlight": "^2.23.1",
+ "@tiptap/extension-history": "^2.23.1",
+ "@tiptap/extension-image": "^2.23.1",
+ "@tiptap/extension-italic": "^2.23.1",
+ "@tiptap/extension-list-item": "^2.23.1",
+ "@tiptap/extension-ordered-list": "^2.23.1",
+ "@tiptap/extension-paragraph": "^2.23.1",
+ "@tiptap/extension-strike": "^2.23.1",
+ "@tiptap/extension-subscript": "^2.23.1",
+ "@tiptap/extension-superscript": "^2.23.1",
+ "@tiptap/extension-text": "^2.23.1",
+ "@tiptap/extension-text-align": "^2.23.1",
+ "@tiptap/extension-text-style": "^2.23.1",
+ "@tiptap/extension-underline": "^2.23.1",
+ "ngx-tiptap": "^12.0.0",
+ "prosemirror-state": "^1.3.4",
+ "rxjs": "~7.8.0"
+ }
+ },
+ "node_modules/@iqb/ngx-coding-components/node_modules/@types/node": {
+ "version": "20.19.43",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz",
+ "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "node_modules/@iqb/ngx-coding-components/node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "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/@iqb/ngx-coding-components/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==",
+ "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/@iqb/ngx-coding-components/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==",
+ "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/@iqb/ngx-coding-components/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/@iqb/ngx-coding-components/node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/@iqb/ngx-coding-components/node_modules/undici-types": {
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
+ },
"node_modules/@iqb/responses": {
+ "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",
+ "@iqbspecs/response": "^2.0.0",
+ "@iqbspecs/variable-info": "^1.3.0",
+ "mathjs": "^15.1.0"
+ }
+ },
+ "node_modules/@iqb/responses-legacy-reference": {
+ "name": "@iqb/responses",
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@iqb/responses/-/responses-5.2.0.tgz",
"integrity": "sha512-Pk1s0CFA/SeAdns/RdHEKMSt/WhHYRzM7jkU7p5n/Nt+1Hpwj4zkvS8uXfofNplYhV7lkeMes1kcceqqQ2fekw==",
+ "dev": true,
"license": "CC0 1.0",
"dependencies": {
"mathjs": "^12.4.2"
}
},
+ "node_modules/@iqb/responses/node_modules/@iqbspecs/response": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@iqbspecs/response/-/response-2.0.0.tgz",
+ "integrity": "sha512-PSEqXhFTGodpmkxkDD8aBWbNDi0R+308k2lvZwMO8lrDvsKA5CXQCmrEzYfNQoTlTaR9wwO+PHWpuHtNmTL6gw==",
+ "license": "CC0 1.0"
+ },
+ "node_modules/@iqb/responses/node_modules/mathjs": {
+ "version": "15.2.0",
+ "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-15.2.0.tgz",
+ "integrity": "sha512-UAQzSVob9rNLdGpqcFMYmSu9dkuLYy7Lr2hBEQS5SHQdknA9VppJz3cy2KkpMzTODunad6V6cNv+5kOLsePLow==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.26.10",
+ "complex.js": "^2.2.5",
+ "decimal.js": "^10.4.3",
+ "escape-latex": "^1.2.0",
+ "fraction.js": "^5.2.1",
+ "javascript-natural-sort": "^0.7.1",
+ "seedrandom": "^3.0.5",
+ "tiny-emitter": "^2.1.0",
+ "typed-function": "^4.2.1"
+ },
+ "bin": {
+ "mathjs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
"node_modules/@iqbspecs/coding-scheme": {
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/@iqbspecs/coding-scheme/-/coding-scheme-3.4.1.tgz",
@@ -10065,6 +10320,17 @@
"url": "https://opencollective.com/pkgr"
}
},
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
"node_modules/@prisma/debug": {
"version": "5.22.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz",
@@ -10115,6 +10381,13 @@
"@prisma/debug": "5.22.0"
}
},
+ "node_modules/@remirror/core-constants": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz",
+ "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@rolldown/binding-android-arm64": {
"version": "1.0.0-rc.4",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.4.tgz",
@@ -11240,45 +11513,423 @@
"node": "^20.17.0 || >=22.9.0"
}
},
- "node_modules/@sinclair/typebox": {
- "version": "0.34.48",
- "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
- "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
- "dev": true,
- "license": "MIT"
+ "node_modules/@sinclair/typebox": {
+ "version": "0.34.48",
+ "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
+ "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+ "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "15.4.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz",
+ "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1"
+ }
+ },
+ "node_modules/@sqltools/formatter": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
+ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==",
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "license": "MIT"
+ },
+ "node_modules/@tiptap/core": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.3.tgz",
+ "integrity": "sha512-a5LfRbLpfaGI3hbL/LPHUYHI0I+FQHdSHsy8L4YnVIuu3hXcm3QZgkWbpEGf8hCz8krk6zEiu0+iFOjTySU2FA==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-blockquote": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.27.3.tgz",
+ "integrity": "sha512-NwK7FUFF0CKGJt/qNDR7plL/9vkJQBy8ziiyn8lk3j/j6tAFlCpgBMj420A1T26ItDpwOCyyaq+PMWHML+E4VQ==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-bold": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.27.3.tgz",
+ "integrity": "sha512-d5tQLAl5nHNrHNkBEgHJ0GYQ52iAsq83fUiKnDxPWuF6leKMOtkUBt9rw918p/L6MPg/MHROZ3Qt4Q+lmVYbfQ==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-bubble-menu": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.27.3.tgz",
+ "integrity": "sha512-08dt5pG9j3NTID1BnKT+FlfVgf16BPXEYtDCmPMLQq/jKHlOP49SRBAGhvYHZt8K7xkj0+JYhcgcJfytukTcKQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tippy.js": "^6.3.7"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-bullet-list": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.27.3.tgz",
+ "integrity": "sha512-LEYkcuCCHYDm6NHWZRIl3Lpac1jXFhABZZpEj+V/ypPgPwKQSIzUMCySHXXNCUQOK3ipF0Z+O18kFnekag8ZLQ==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-color": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-2.27.3.tgz",
+ "integrity": "sha512-+xHveJ8YfneusZIp87/8UW7VfdtCVgE5iN3JkTudC1TdYCnMqxbaIwQaGtsBH+UxinXhJSU9tcsD1P6QgKJSqg==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/extension-text-style": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-document": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.27.3.tgz",
+ "integrity": "sha512-U10TnvBa6WTjBu64U4gn/HaxkBs/96q0U6mKH0PO9Ab0n3Bf/iN4HMtix2TEa3qWlJ8peVk9/RBwjyxNVNub4w==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-floating-menu": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.27.3.tgz",
+ "integrity": "sha512-4Be2efRPxLqZT0QB/IStVdVR1wP+FVPQxw/7qJ+xRk+m7EX45LVO0hvRdMK9aMO8Yd8JK2Wh9V3DbCN6cZ89vw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tippy.js": "^6.3.7"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-font-family": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-font-family/-/extension-font-family-2.27.3.tgz",
+ "integrity": "sha512-TwTUCxv2uFfYItW8xMrzVjJaH8KNZXwY9KF1a9PtxtJmFCylJBkYst+V0QYZlgseQfes/tZjZyiGd1QQcgTrgQ==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/extension-text-style": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-heading": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.27.3.tgz",
+ "integrity": "sha512-VWcj9b5VAhMJSAeG8xZyzCaVCcwmBGJL2k77lE2ZQaCS/jSvKUTN5ntd/1vwLrmaduciB7FmdVgOmWFcODyX1Q==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-highlight": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-highlight/-/extension-highlight-2.27.3.tgz",
+ "integrity": "sha512-NalKhoLGfXelD1P+2wThcpiorINDqeNz05brko28Tg+Ue9skOKO54Uh0XCoe93Mig/MvP/KQbi578LCYz8mqmw==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-history": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.27.3.tgz",
+ "integrity": "sha512-btT7Teg9xtWbv9q6uc38aIQbZsbMhgNu1NGLf7nLf7Vqzm+GRCpRF5EzlF9RpwEPtC2j1bxrDzpy427Su1qF7w==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-image": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-2.27.3.tgz",
+ "integrity": "sha512-YCOxC+UOFisHVpowPc3UmUtJDV9tjKJLeHKef3DZ6h1ixOnp2LStuhy2ohcSjISg1mZlYrx+/GoYottjwj7pww==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-italic": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.27.3.tgz",
+ "integrity": "sha512-ycgP6h7QQ4WXojOlH7gQEWwzNEzkIkVzdfH6jAZtZ2nw71L5KO1t5kt6Iu382CmQa60skDTuErbfiBUx7a3e2A==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-list-item": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.27.3.tgz",
+ "integrity": "sha512-jWh5tZdNiZDx8X3jKV80EM6zMfUeRD3HKNVGcj5izqNncgH+/jJtF3hDmpP0nbFmhren8BQFo6W5f9L/GqtAVA==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-ordered-list": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.27.3.tgz",
+ "integrity": "sha512-RvxSnE8rpiMSosD7ANCsyA+7XPEz0R6mDOFlORMqAf+NnPfCnH8dguVuKBGOdodPj69NcTSlA/6VBCS+3J0CLw==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-paragraph": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.27.3.tgz",
+ "integrity": "sha512-Nbevu3wZk212NDpp1FaN05UbTHA4NHP2B5bAmkP8z3I+/ITvEgp7p9vFqJxZhe7fmqCSFlzhKj8ibmBc+Iu4CQ==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-strike": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.27.3.tgz",
+ "integrity": "sha512-9Ax1UIRDOdPk/bGnHbKM39Bw+Zb0PV8IInlT6hr8K1or0wWZuiyE4zlKP3bhAB6IcDr4mJLJQ6TEmxKihivC5A==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-subscript": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-subscript/-/extension-subscript-2.27.3.tgz",
+ "integrity": "sha512-x5Q1UVHLVP24wteh0xMSnwQ59WDKwNDOH0HzgbJ/UrDnN4f9H/haflQ4yZL0xdT2cYpvzJGPBugi2iKLcpeZjg==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-superscript": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-superscript/-/extension-superscript-2.27.3.tgz",
+ "integrity": "sha512-dLZ9FBG6w5xlEeEsDR8T0nmI3ZDAYgLdkb/+bj8yOkzIUAbf9eDsE3HgqUrHPIm/7h9arJydnrdgOaxVDVlldg==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-text": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.27.3.tgz",
+ "integrity": "sha512-9VnSK7qXUuZevNzE0FIymbc8BbY5X+0k4NxxO2dtPBOX4IoNHcWzmpLcuCggd3uN6XYLAXhDi/w2hkmBtig8Mw==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-text-align": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.27.3.tgz",
+ "integrity": "sha512-1gUN+rdCkuYDePfY8/AixiftkXZDqDN1czZS+e5QIZCNcYnFPIIVu4qurE9kaE0NYo46/RPhPERFBQAbK+dWFw==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
+ },
+ "node_modules/@tiptap/extension-text-style": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.27.3.tgz",
+ "integrity": "sha512-Z4ZKju7vA2vUCeWVgEWERHvnln6XtgEMcb29xW8i25dSrw5Qefn/b+ym7JPkaMvjWbbQ+5VtECiaEYdL3rRH6Q==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
+ }
},
- "node_modules/@sinonjs/commons": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
- "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "type-detect": "4.0.8"
+ "node_modules/@tiptap/extension-underline": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-2.27.3.tgz",
+ "integrity": "sha512-9qc6WtfF7j+gwlbz7ChmwjPqDRF8T2vbHMrZLoWERAez2pOi9VfRG7lj437sc1v9LPT31FlWyIkaTDTGqYMPWA==",
+ "license": "MIT",
+ "peer": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
+ },
+ "peerDependencies": {
+ "@tiptap/core": "^2.7.0"
}
},
- "node_modules/@sinonjs/fake-timers": {
- "version": "15.4.0",
- "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.4.0.tgz",
- "integrity": "sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==",
- "dev": true,
- "license": "BSD-3-Clause",
+ "node_modules/@tiptap/pm": {
+ "version": "2.27.3",
+ "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.3.tgz",
+ "integrity": "sha512-E9mBCSwe8YdWXvpjRMqddx4Yd5lEj+EprNM+4J+MtBXPGFZCUAa90rkOKU4m+Pn7rd1N5s7wRsExqFco79twbg==",
+ "license": "MIT",
+ "peer": true,
"dependencies": {
- "@sinonjs/commons": "^3.0.1"
+ "prosemirror-changeset": "^2.3.0",
+ "prosemirror-collab": "^1.3.1",
+ "prosemirror-commands": "^1.6.2",
+ "prosemirror-dropcursor": "^1.8.1",
+ "prosemirror-gapcursor": "^1.3.2",
+ "prosemirror-history": "^1.4.1",
+ "prosemirror-inputrules": "^1.4.0",
+ "prosemirror-keymap": "^1.2.2",
+ "prosemirror-markdown": "^1.13.1",
+ "prosemirror-menu": "^1.2.4",
+ "prosemirror-model": "^1.23.0",
+ "prosemirror-schema-basic": "^1.2.3",
+ "prosemirror-schema-list": "^1.4.1",
+ "prosemirror-state": "^1.4.3",
+ "prosemirror-tables": "^1.6.4",
+ "prosemirror-trailing-node": "^3.0.0",
+ "prosemirror-transform": "^1.10.2",
+ "prosemirror-view": "^1.42.3"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/ueberdosis"
}
},
- "node_modules/@sqltools/formatter": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/@sqltools/formatter/-/formatter-1.2.5.tgz",
- "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==",
- "license": "MIT"
- },
- "node_modules/@standard-schema/spec": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
- "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
- "license": "MIT"
- },
"node_modules/@tokenizer/inflate": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
@@ -11665,12 +12316,37 @@
"@types/node": "*"
}
},
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/luxon": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.7.1.tgz",
"integrity": "sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg==",
"license": "MIT"
},
+ "node_modules/@types/markdown-it": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.2.0.tgz",
+ "integrity": "sha512-NoQ2yGlLWj4wpxMs+TYmRKk3thDrQ97agr7sFqfLsAlvoS8SNQuTrlObhFqG9iugdTtgOE9jpJ6FNM4ZGsa5xQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/mime": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@@ -15629,6 +16305,13 @@
"devOptional": true,
"license": "MIT"
},
+ "node_modules/crelt": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
+ "integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/cron": {
"version": "4.3.5",
"resolved": "https://registry.npmjs.org/cron/-/cron-4.3.5.tgz",
@@ -17084,7 +17767,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -18551,7 +19233,6 @@
"version": "5.3.4",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
"integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": "*"
@@ -19663,17 +20344,15 @@
}
},
"node_modules/image-size": {
- "version": "0.5.5",
- "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz",
- "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==",
- "dev": true,
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.3.tgz",
+ "integrity": "sha512-9BJAtVCBth+76NwqnEfWr3PiRnYhbquk6K/USuQ1GQajz5NgLzO7tNsnoNjHfO7LuftgkBf+CCH6j28hjMbIYQ==",
"license": "MIT",
- "optional": true,
"bin": {
"image-size": "bin/image-size.js"
},
"engines": {
- "node": ">=0.10.0"
+ "node": ">=18"
}
},
"node_modules/immediate": {
@@ -22154,6 +22833,26 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
+ "node_modules/linkify-it": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz",
+ "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
"node_modules/listenercount": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
@@ -22787,6 +23486,34 @@
"node": ">= 0.6"
}
},
+ "node_modules/markdown-it": {
+ "version": "14.3.2",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz",
+ "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/puzrin"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/markdown-it"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.5.0",
+ "linkify-it": "^5.0.2",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -22845,6 +23572,13 @@
"dev": true,
"license": "CC0-1.0"
},
+ "node_modules/mdurl": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
+ "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -23430,6 +24164,31 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/ngx-tiptap": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/ngx-tiptap/-/ngx-tiptap-12.0.0.tgz",
+ "integrity": "sha512-m8jngTbQZWCMDSogwMcXaNpIpNvCx47D6hWM6lgmOLR2UC5vvvphQmDuC1t66Cn4brdol/vUVVFBbgqo56Jsmw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.3.0"
+ },
+ "peerDependencies": {
+ "@angular/common": ">=19.0.0",
+ "@angular/core": ">=19.0.0",
+ "@angular/forms": ">=19.0.0",
+ "@tiptap/core": ">=2.7.0",
+ "@tiptap/extension-bubble-menu": "^2.7.0",
+ "@tiptap/extension-floating-menu": "^2.7.0",
+ "@tiptap/pm": "^2.7.0"
+ }
+ },
"node_modules/node-abi": {
"version": "3.87.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz",
@@ -24843,6 +25602,13 @@
"license": "MIT",
"optional": true
},
+ "node_modules/orderedmap": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
+ "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/ospath": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/ospath/-/ospath-1.2.2.tgz",
@@ -26666,6 +27432,219 @@
"node": ">= 4"
}
},
+ "node_modules/prosemirror-changeset": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.4.tgz",
+ "integrity": "sha512-Gyuz9jYdrAeA9I7AholyOes9KUmv/lEwsC5O9s5xF5VB3EgTPsXMGUxz4+H1B+wKVPdZbvRcg1Cp3x8qX+8ofA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-transform": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-collab": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz",
+ "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-state": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-commands": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.2.tgz",
+ "integrity": "sha512-q6Q6szxqdu9Xd6EcdKsqXghu5nQdZTpB4Q9yd04WRc7/jt763e/rT60Owh0L1GYY+T46o5rD+9lEN36dZS43tw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.10.2"
+ }
+ },
+ "node_modules/prosemirror-dropcursor": {
+ "version": "1.8.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.4.tgz",
+ "integrity": "sha512-VmKxhcA6a+h5oPSYmfF1ZNeQqPbyYaIxqZsKqeYCyTlPtrjUgNaHL3P8vVaO19oqAV0zuaM/axkowZHrB1P3Tg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0",
+ "prosemirror-view": "^1.1.0"
+ }
+ },
+ "node_modules/prosemirror-gapcursor": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
+ "integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-keymap": "^1.0.0",
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-view": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-history": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.1.tgz",
+ "integrity": "sha512-onlwnqKvgtFg6srZOmDOAhlAy1FIHQpOExORybX0S+DvJepE5tPALrB9iGet/2sqqqrelSnB10OoiOJqAvu2DA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-state": "^1.2.2",
+ "prosemirror-transform": "^1.0.0",
+ "prosemirror-view": "^1.31.0",
+ "rope-sequence": "^1.3.0"
+ }
+ },
+ "node_modules/prosemirror-inputrules": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz",
+ "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-keymap": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
+ "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-state": "^1.0.0",
+ "w3c-keyname": "^2.2.0"
+ }
+ },
+ "node_modules/prosemirror-markdown": {
+ "version": "1.13.8",
+ "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.8.tgz",
+ "integrity": "sha512-Zi43BXX28jmxDJKvk8TDS2gMkBY/mgP8ynd6l/Vxrv8rgNFs5hsV1G0VscxTID1UCzVPcidADP9fUOBY3xxUeA==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/markdown-it": "^14.0.0",
+ "markdown-it": "^14.0.0",
+ "prosemirror-model": "^1.25.0"
+ }
+ },
+ "node_modules/prosemirror-menu": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.3.2.tgz",
+ "integrity": "sha512-6VgUJTYod0nMBlCaYJGhXGLu7Gt4AvcwcOq0YfJCY/6Uh+3S7UsWhpy6rJFCBFOmonq1hD8KyWOtZhkppd4YPg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "crelt": "^1.0.0",
+ "prosemirror-commands": "^1.0.0",
+ "prosemirror-history": "^1.0.0",
+ "prosemirror-state": "^1.0.0"
+ }
+ },
+ "node_modules/prosemirror-model": {
+ "version": "1.25.12",
+ "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.12.tgz",
+ "integrity": "sha512-Ue2gTmXMa7EhpLNhC7J+h4+ykD8ha12K6rrZFFKKJHBForfIStw5gJ6Zrf1mqqAa9NmpmEB4wp9bKmA4eUVjWg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "orderedmap": "^2.0.0"
+ }
+ },
+ "node_modules/prosemirror-schema-basic": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.5.tgz",
+ "integrity": "sha512-McBWhfgOfThyu3Nltbs8SOUbM4t/h0pxteibaaHd1QR4S+F1t0u7M1ETaCE6/cveFoF+2fowTIBNkJOyeZQjNg==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-model": "^1.25.0"
+ }
+ },
+ "node_modules/prosemirror-schema-list": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
+ "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.7.3"
+ }
+ },
+ "node_modules/prosemirror-state": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
+ "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-model": "^1.0.0",
+ "prosemirror-transform": "^1.0.0",
+ "prosemirror-view": "^1.27.0"
+ }
+ },
+ "node_modules/prosemirror-tables": {
+ "version": "1.8.5",
+ "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
+ "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-keymap": "^1.2.3",
+ "prosemirror-model": "^1.25.4",
+ "prosemirror-state": "^1.4.4",
+ "prosemirror-transform": "^1.10.5",
+ "prosemirror-view": "^1.41.4"
+ }
+ },
+ "node_modules/prosemirror-trailing-node": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz",
+ "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@remirror/core-constants": "3.0.0",
+ "escape-string-regexp": "^4.0.0"
+ },
+ "peerDependencies": {
+ "prosemirror-model": "^1.22.1",
+ "prosemirror-state": "^1.4.2",
+ "prosemirror-view": "^1.33.8"
+ }
+ },
+ "node_modules/prosemirror-transform": {
+ "version": "1.12.2",
+ "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.2.tgz",
+ "integrity": "sha512-PE/aY0HEY4zczvmqrilgkUK/WautF0chvMqkmM/iN5/aPHvwrCWaQZjS6CIZp+K84YrvPFqrL+YArrfJX8xX7g==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-model": "^1.21.0"
+ }
+ },
+ "node_modules/prosemirror-view": {
+ "version": "1.42.6",
+ "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.42.6.tgz",
+ "integrity": "sha512-3g25f6hKV2bBxo3puH3EcYWmmVNSH0iWmNsMVT7sugDSpA025jgpOLarcDWr39SdzPjYMaiYtRdNtXialPSjEQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "prosemirror-model": "^1.25.12",
+ "prosemirror-state": "^1.0.0",
+ "prosemirror-transform": "^1.1.0"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz",
@@ -26729,6 +27708,16 @@
"node": ">=6"
}
},
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/pure-rand": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
@@ -27390,6 +28379,13 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/rope-sequence": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
+ "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/router": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
@@ -29701,6 +30697,16 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tippy.js": {
+ "version": "6.3.7",
+ "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
+ "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@popperjs/core": "^2.9.0"
+ }
+ },
"node_modules/tldts": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
@@ -31045,6 +32051,13 @@
"node": ">=14.17"
}
},
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
@@ -31561,6 +32574,13 @@
}
}
},
+ "node_modules/w3c-keyname": {
+ "version": "2.2.8",
+ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
+ "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
diff --git a/package.json b/package.json
index 7ee247a53..4f9858bc8 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
"node": ">=22.12.0 <23"
},
"dependencies": {
+ "@angular/animations": "21.2.24",
"@angular/cdk": "21.2.14",
"@angular/common": "21.2.24",
"@angular/compiler": "21.2.24",
@@ -40,7 +41,8 @@
"@angular/router": "21.2.24",
"@iqb/metadata-components": "1.0.0",
"@iqb/metadata-resolver": "0.2.0",
- "@iqb/responses": "^5.2.0",
+ "@iqb/ngx-coding-components": "file:vendor/iqb-ngx-coding-components-4.1.0-dev.ff15d6d.tgz",
+ "@iqb/responses": "5.3.1",
"@iqbspecs/coding-scheme": "^3.4.1",
"@iqbspecs/metadata-profile": "^0.11.1",
"@iqbspecs/metadata-values": "^3.0.1",
@@ -88,6 +90,7 @@
"keycloak-angular": "21.0.0",
"keycloak-js": "^26.2.4",
"libxmljs2": "^0.37.0",
+ "mathjs": "12.4.3",
"mathml2omml": "^0.5.0",
"multer": "2.4.0",
"passport-jwt": "^4.0.1",
@@ -115,6 +118,7 @@
"@angular/compiler-cli": "21.2.24",
"@golevelup/ts-jest": "^0.5.0",
"@iqb/eslint-config": "^2.1.1",
+ "@iqb/responses-legacy-reference": "npm:@iqb/responses@5.2.0",
"@nx/angular": "22.7.12",
"@nx/cypress": "22.7.12",
"@nx/esbuild": "22.7.12",
@@ -203,7 +207,8 @@
"compression": "1.8.2",
"proxy-addr": "2.0.8",
"source-map-js": "1.2.2",
- "handlebars": "4.7.10"
+ "handlebars": "4.7.10",
+ "image-size": "2.0.3"
},
"eslintConfig": {
"extends": "@iqb/eslint-config",
diff --git a/scripts/build-autocoder-compat.mjs b/scripts/build-autocoder-compat.mjs
new file mode 100644
index 000000000..40a140915
--- /dev/null
+++ b/scripts/build-autocoder-compat.mjs
@@ -0,0 +1,53 @@
+import assert from 'node:assert/strict';
+import { createRequire } from 'node:module';
+import { readFile, writeFile, mkdir } from 'node:fs/promises';
+import { dirname, join, sep } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { build } from 'esbuild';
+
+// Rebuild after reviewing an installed Responses upgrade. esbuild bundles its
+// sources unchanged and redirects only the mathjs import to our solver adapter.
+const root = fileURLToPath(new URL('../', import.meta.url));
+const require = createRequire(join(root, 'package.json'));
+const responsesManifest = require.resolve('@iqb/responses/package.json');
+const responsesRoot = dirname(responsesManifest);
+assert.equal(require('@iqb/responses/package.json').version, '5.3.1');
+assert.equal(require('mathjs/package.json').version, '12.4.3');
+const output = join(root, 'apps/backend/src/app/database/services/coding/generated/autocoder-compatibility-engine.js');
+const mathAdapter = join(root, 'apps/backend/src/app/database/services/coding/autocoder-solver-math.ts');
+let redirectedImports = 0;
+const result = await build({
+ absWorkingDir: root,
+ entryPoints: [require.resolve('@iqb/responses/coding-scheme-factory')],
+ bundle: true,
+ platform: 'node',
+ format: 'cjs',
+ target: 'node22',
+ write: false,
+ external: ['@iqbspecs/coding-scheme'],
+ banner: { js: '// Generated by node scripts/build-autocoder-compat.mjs; do not edit.\n' +
+ '// Engine: @iqb/responses 5.3.1, IQB, CC0-1.0. Only solver math is scoped to\n' +
+ '// autocoder-solver-math.ts / mathjs 12.4.3 (Apache-2.0), preserving 5.2.0 behaviour.' },
+ plugins: [{
+ name: 'isolated-legacy-solver-math',
+ setup(builder) {
+ builder.onResolve({ filter: /^mathjs$/ }, args => {
+ if (args.importer.startsWith(responsesRoot + sep)) {
+ redirectedImports += 1;
+ return { path: mathAdapter };
+ }
+ return { path: 'mathjs', external: true };
+ });
+ }
+ }]
+});
+assert.equal(redirectedImports, 1, 'Review the Responses dependency graph before changing this adapter');
+const content = result.outputFiles[0].text;
+if (process.argv.includes('--check')) {
+ assert.equal(await readFile(output, 'utf8'), content,
+ 'Stale compatibility bundle: run node scripts/build-autocoder-compat.mjs');
+ console.log('PASS: generated engine matches installed Responses 5.3.1 and isolated solver adapter');
+} else {
+ await mkdir(dirname(output), { recursive: true });
+ await writeFile(output, content);
+}
diff --git a/vendor/iqb-ngx-coding-components-4.1.0-dev.ff15d6d.tgz b/vendor/iqb-ngx-coding-components-4.1.0-dev.ff15d6d.tgz
new file mode 100644
index 000000000..5b00dccf6
Binary files /dev/null and b/vendor/iqb-ngx-coding-components-4.1.0-dev.ff15d6d.tgz differ