Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions apps/teacher-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,13 @@
"primeicons": "^7.0.0",
"primevue": "^4.5.5",
"vue": "^3.5.34",
"vue-i18n": "^11.4.2",
"vue-router": "^5.0.6"
"vue-i18n": "^11.4.4",
"vue-router": "^5.0.7"
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^25.7.0",
"@vitejs/plugin-vue": "^6.0.6",
"@types/node": "^25.9.0",
"@vitejs/plugin-vue": "^6.0.7",
"@vue/tsconfig": "^0.9.1",
"@vue/vue3-jest": "^29.2.6",
"identity-obj-proxy": "^3.0.0",
Expand All @@ -36,7 +36,7 @@
"jsdom": "^29.1.1",
"ts-jest": "^29.4.9",
"typescript": "^6.0.3",
"vite": "^8.0.12",
"vue-tsc": "^3.2.9"
"vite": "^8.0.13",
"vue-tsc": "^3.3.0"
}
}
14 changes: 11 additions & 3 deletions apps/teacher-ui/src/composables/useExamsBridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,8 +210,16 @@ export function initializeExamsBridge(): ExamsBridge {
exportCorrectionSheetsPdfUseCase.exportAllCandidatesPdf(examId),
exportCorrectionSession: (input) =>
exportCorrectionSessionArtifactsUseCase.execute(input),
importCorrectionBundle: (input) =>
importKbrCorrectionBundleUseCase.execute(input),
importCorrectionBundle: (input) => {
if (Array.isArray(input.bundle)) {
return importKbrCorrectionBundleUseCase.executeMany({
...input,
bundles: input.bundle
});
}

return importKbrCorrectionBundleUseCase.execute(input);
},
Comment on lines +213 to +222
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When input.bundle is an array, spreading input directly into executeMany means the duplicate bundle property (which is the array) is still passed along with the new bundles property. Destructuring bundle out of input first keeps the payload clean and avoids passing redundant properties.

Suggested change
importCorrectionBundle: (input) => {
if (Array.isArray(input.bundle)) {
return importKbrCorrectionBundleUseCase.executeMany({
...input,
bundles: input.bundle
});
}
return importKbrCorrectionBundleUseCase.execute(input);
},
importCorrectionBundle: (input) => {
if (Array.isArray(input.bundle)) {
const { bundle, ...rest } = input;
return importKbrCorrectionBundleUseCase.executeMany({
...rest,
bundles: bundle
});
}
return importKbrCorrectionBundleUseCase.execute(input);
},


initialized: true
};
Expand Down Expand Up @@ -288,4 +296,4 @@ export function useExamsBridge(): UseExamsBridgeResult {
importCorrectionBundle: (input: any) =>
bridge.value?.importCorrectionBundle(input) ?? Promise.reject(new Error('Bridge not initialized'))
};
}
}
2 changes: 1 addition & 1 deletion modules/exams/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
},
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^25.7.0",
"@types/node": "^25.9.0",
"@types/uuid": "^11.0.0",
"jest": "^30.4.2",
"ts-jest": "^29.4.9",
Expand Down
7 changes: 4 additions & 3 deletions modules/exams/rule-packs/default/contract.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@

## Expected Return Format

- `Zwischenexport` and `Ende Korrektur` must return raw JSON only when a valid import-bundle export can be produced.
- The returned JSON must conform to the loaded import bundle schema.
- No YAML, CSV, Markdown table, prose summary, or substitute export format is allowed when emitting JSON.
- `Zwischenexport` must return raw JSON only when one valid import-bundle export can be produced.
- `Ende Korrektur` must return one raw JSON array containing one import bundle object per resolved Leistung.
- Every import bundle object must conform to the loaded import bundle schema.
- No YAML, CSV, Markdown table, prose summary, wrapper object, or substitute export format is allowed when emitting JSON.
- If a valid import-bundle JSON export cannot be produced, output exactly one short plain-text line stating the missing prerequisite, and nothing else.

## Chat References
Expand Down
16 changes: 11 additions & 5 deletions modules/exams/rule-packs/default/prompt.template.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ Session workflow (generic and strict):
Chat reference roles:
- the contract's `Session Chat Reference` identifies the session/contract only
- Leistung chatRefs are internal import/export keys for submitted Leistungen and look like `chat-0001`
- the import bundle top-level `chatRef` must be the resolved Leistung chatRef from the contract's `Chat References` list
- every import bundle object must use the resolved Leistung `chatRef` from the contract's `Chat References` list as its top-level `chatRef`
- never ask the user to provide a Leistung `chatRef`
- never write the `Session Chat Reference` into the import bundle top-level `chatRef`
- never write the `Session Chat Reference` into an import bundle top-level `chatRef`

Matching rule:
- extract the needed matching information from the submitted Leistung itself
Expand All @@ -35,13 +35,15 @@ Matching rule:

Control commands in this session:
- `Zwischenexport`: output current result state for the active resolved Leistung `chatRef`
- `Ende Korrektur`: finish the session cleanly after current Leistung
- `Ende Korrektur`: finish the session cleanly after current Leistung and export all resolved Leistungen from this session
- `Verwirf letzte Arbeit`: discard only the last processed Leistung for the active resolved Leistung `chatRef`

Output format requirements:
- `Zwischenexport` must return exactly one raw JSON object for the active resolved Leistung `chatRef` when a valid export can be produced
- `Ende Korrektur` must return exactly one raw JSON object for the final export when a valid export can be produced
- the returned JSON must conform to the loaded import bundle schema
- `Ende Korrektur` must return exactly one raw JSON array; each array item must be one import bundle object for one resolved Leistung `chatRef`
- every object in that array must conform to the loaded import bundle schema below
- do not invent a wrapper object for multi-Leistung export
- do not include unresolved, unprocessed, or non-matching Leistungen in the JSON array
- do not output YAML, CSV, Markdown tables, prose summaries, or any substitute export format when emitting JSON
- do not wrap JSON in Markdown code fences
- do not prepend or append explanatory text when emitting JSON for an export command
Expand All @@ -58,6 +60,10 @@ Required import bundle fields:

{{contractMarkdown}}

## Import Bundle Schema

{{importBundleSchema}}

## Rule Pack Metadata

{{rulePackManifest}}
Expand Down
23 changes: 15 additions & 8 deletions modules/exams/src/rule-packs/default-pack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ const DEFAULT_CONTRACT_TEMPLATE = `# Correction Session Contract

## Expected Return Format

- \`Zwischenexport\` and \`Ende Korrektur\` must return raw JSON only when a valid import-bundle export can be produced.
- The returned JSON must conform to the loaded import bundle schema.
- No YAML, CSV, Markdown table, prose summary, or substitute export format is allowed when emitting JSON.
- \`Zwischenexport\` must return raw JSON only when a valid import-bundle export can be produced.
- \`Ende Korrektur\` must return one raw JSON array containing one import bundle object per resolved Leistung.
- Every import bundle object must conform to the loaded import bundle schema.
- No YAML, CSV, Markdown table, prose summary, wrapper object, or substitute export format is allowed when emitting JSON.
- If a valid import-bundle JSON export cannot be produced, output exactly one short plain-text line stating the missing prerequisite, and nothing else.

## Chat References
Expand Down Expand Up @@ -142,9 +143,9 @@ Session workflow (generic and strict):
Chat reference roles:
- the contract's \`Session Chat Reference\` identifies the session/contract only
- Leistung chatRefs are internal import/export keys for submitted Leistungen and look like \`chat-0001\`
- the import bundle top-level \`chatRef\` must be the resolved Leistung chatRef from the contract's \`Chat References\` list
- every import bundle object must use the resolved Leistung \`chatRef\` from the contract's \`Chat References\` list as its top-level \`chatRef\`
- never ask the user to provide a Leistung \`chatRef\`
- never write the \`Session Chat Reference\` into the import bundle top-level \`chatRef\`
- never write the \`Session Chat Reference\` into an import bundle top-level \`chatRef\`

Matching rule:
- extract the needed matching information from the submitted Leistung itself
Expand All @@ -158,13 +159,15 @@ Matching rule:

Control commands in this session:
- \`Zwischenexport\`: output current result state for the active resolved Leistung \`chatRef\`
- \`Ende Korrektur\`: finish the session cleanly after current Leistung
- \`Ende Korrektur\`: finish the session cleanly after current Leistung and export all resolved Leistungen from this session
- \`Verwirf letzte Arbeit\`: discard only the last processed Leistung for the active resolved Leistung \`chatRef\`

Output format requirements:
- \`Zwischenexport\` must return exactly one raw JSON object for the active resolved Leistung \`chatRef\` when a valid export can be produced
- \`Ende Korrektur\` must return exactly one raw JSON object for the final export when a valid export can be produced
- the returned JSON must conform to the loaded import bundle schema
- \`Ende Korrektur\` must return exactly one raw JSON array; each array item must be one import bundle object for one resolved Leistung \`chatRef\`
- every object in that array must conform to the loaded import bundle schema below
- do not invent a wrapper object for multi-Leistung export
- do not include unresolved, unprocessed, or non-matching Leistungen in the JSON array
- do not output YAML, CSV, Markdown tables, prose summaries, or any substitute export format when emitting JSON
- do not wrap JSON in Markdown code fences
- do not prepend or append explanatory text when emitting JSON for an export command
Expand All @@ -181,6 +184,10 @@ Required import bundle fields:

{{contractMarkdown}}

## Import Bundle Schema

{{importBundleSchema}}

## Rule Pack Metadata

{{rulePackManifest}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ function buildPromptArtifacts(
): string {
return renderTemplate(rulePack.templates.prompt, {
contractMarkdown,
importBundleSchema: JSON.stringify(rulePack.importBundleSchema, null, 2),
rulePackManifest: JSON.stringify(rulePack.manifest, null, 2),
rulePackRules: JSON.stringify(rulePack.rules, null, 2),
'session.id': contract.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ export interface ImportKbrCorrectionBundleInput {
finalizeCorrection?: boolean;
}

export interface ImportKbrCorrectionBundleBatchInput extends Omit<ImportKbrCorrectionBundleInput, 'bundle'> {
bundles: unknown[];
}

export interface ImportKbrCorrectionBundleResult {
correction: Exams.CorrectionEntry;
candidateId: string;
Expand All @@ -53,6 +57,14 @@ export interface ImportKbrCorrectionBundleResult {
uncertainties: CorrectionImportUncertainty[];
}

export interface ImportKbrCorrectionBundleBatchResult {
results: ImportKbrCorrectionBundleResult[];
importedBundleCount: number;
importedTaskScoreCount: number;
skippedTaskScoreCount: number;
uncertainties: CorrectionImportUncertainty[];
}

function asRecord(value: unknown): Record<string, unknown> | undefined {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return undefined;
Expand Down Expand Up @@ -209,6 +221,22 @@ export class ImportKbrCorrectionBundleUseCase {
private readonly recordCorrectionUseCase: RecordCorrectionUseCase
) {}

async executeMany(input: ImportKbrCorrectionBundleBatchInput): Promise<ImportKbrCorrectionBundleBatchResult> {
const results: ImportKbrCorrectionBundleResult[] = [];

for (const bundle of input.bundles) {
results.push(await this.execute({ ...input, bundle }));
}

return {
results,
importedBundleCount: results.length,
importedTaskScoreCount: results.reduce((sum, result) => sum + result.importedTaskScoreCount, 0),
skippedTaskScoreCount: results.reduce((sum, result) => sum + result.skippedTaskScoreCount, 0),
uncertainties: results.flatMap((result) => result.uncertainties)
};
}
Comment on lines +224 to +238
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Spreading input directly into this.execute passes the entire bundles array to each individual execute call as an extra property, which is unnecessary and can be avoided by destructuring bundles first.

Additionally, since this method processes multiple bundles sequentially using await in a loop, any error thrown by a single bundle (e.g., validation failure or database error) will abort the entire batch import midway. This can leave the database in a partially imported state. Consider wrapping the batch execution in a database transaction if atomicity is required, or catching errors per bundle to allow successful ones to complete while returning a list of failed imports.

Suggested change
async executeMany(input: ImportKbrCorrectionBundleBatchInput): Promise<ImportKbrCorrectionBundleBatchResult> {
const results: ImportKbrCorrectionBundleResult[] = [];
for (const bundle of input.bundles) {
results.push(await this.execute({ ...input, bundle }));
}
return {
results,
importedBundleCount: results.length,
importedTaskScoreCount: results.reduce((sum, result) => sum + result.importedTaskScoreCount, 0),
skippedTaskScoreCount: results.reduce((sum, result) => sum + result.skippedTaskScoreCount, 0),
uncertainties: results.flatMap((result) => result.uncertainties)
};
}
async executeMany(input: ImportKbrCorrectionBundleBatchInput): Promise<ImportKbrCorrectionBundleBatchResult> {
const { bundles, ...rest } = input;
const results: ImportKbrCorrectionBundleResult[] = [];
for (const bundle of bundles) {
results.push(await this.execute({ ...rest, bundle }));
}
return {
results,
importedBundleCount: results.length,
importedTaskScoreCount: results.reduce((sum, result) => sum + result.importedTaskScoreCount, 0),
skippedTaskScoreCount: results.reduce((sum, result) => sum + result.skippedTaskScoreCount, 0),
uncertainties: results.flatMap((result) => result.uncertainties)
};
}


async execute(input: ImportKbrCorrectionBundleInput): Promise<ImportKbrCorrectionBundleResult> {
const schema = input.schema ?? getDefaultCorrectionImportBundleSchema();
const { bundle } = validateCorrectionImportBundle(input.bundle, schema);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,13 @@ describe('ExportCorrectionSessionArtifactsUseCase', () => {
expect(artifact.contractFile.content).toContain('expectedHorizon');
expect(artifact.contractFile.content).toContain('Erwartungshorizont');
expect(artifact.promptFile.content).toContain('do not invent or replace them');
expect(artifact.promptFile.content).toContain('## Import Bundle Schema');
expect(artifact.promptFile.content).toContain('"required": [');
expect(artifact.promptFile.content).toContain('"contract"');
expect(artifact.promptFile.content).toContain('"chatRef"');
expect(artifact.promptFile.content).toContain('"importedTaskScores"');
expect(artifact.promptFile.content).toContain('must return exactly one raw JSON array');
expect(artifact.promptFile.content).toContain('do not invent a wrapper object');
});

it('does not require users to provide Leistung chatRefs before evaluation', () => {
Expand Down
Loading
Loading