feat: publish verified five-bank question dataset

This commit is contained in:
suanyuit-cyber
2026-10-29 23:09:32 +08:00
parent c3edf3975e
commit 26e0a920b6
26 changed files with 109610 additions and 1560 deletions
+77 -4
View File
@@ -34,9 +34,18 @@ test('same ID with different content is not treated as the answer', () => {
assert.equal(core.mapAnswers(record, current).status, 'unmapped');
});
test('duplicate option content is ambiguous even when ID exists', () => {
test('duplicate option content maps only with complete unchanged option ID identities', () => {
const record = core.normalizeRecord(fixture({ options: [{ id: 'x', text: '提高温度', label: 'A' }, { id: 'y', text: '提高温度', label: 'B' }] }));
assert.equal(core.mapAnswers(record, record).status, 'ambiguous');
assert.deepEqual(core.mapAnswers(record, record), { status: 'matched', labels: ['B'], answerTexts: ['提高温度'] });
const reordered = { ...record, options: [...record.options].reverse().map((option, index) => ({ ...option, label: 'AB'[index] })) };
assert.deepEqual(core.mapAnswers(record, reordered).labels, ['A']);
const newIds = { ...reordered, options: reordered.options.map((option, index) => ({ ...option, id: `new-${index}` })) };
assert.equal(core.mapAnswers(record, newIds).status, 'ambiguous');
const oneChangedId = { ...reordered, options: reordered.options.map((option, index) => index ? { ...option, id: 'new-id' } : option) };
assert.equal(core.mapAnswers(record, oneChangedId).status, 'ambiguous');
assert.equal(core.mapAnswers({ ...record, correctOptionIds: [] }, record).status, 'ambiguous');
assert.equal(core.mapAnswers(record, { ...record, options: record.options.map(option => ({ ...option, id: '' })) }).status, 'ambiguous');
assert.ok(core.toCSV([record]).includes('"B"'), 'self export should preserve the observed correct letter');
});
const repeatedMultiple = (overrides = {}) => fixture({
@@ -73,10 +82,12 @@ test('all correct occurrences of repeated multiple-choice text map after IDs and
assert.deepEqual(core.mapAnswers(subset, current).labels, ['A', 'D']);
});
test('partly correct or text-only repeated options remain ambiguous', () => {
test('partly correct repeated options require complete IDs; text-only repeated options remain ambiguous', () => {
const partial = core.normalizeRecord(repeatedMultiple({ correctOptionIds: ['b'], answerTexts: ['下降'] }));
assert.equal(partial.answerStatus, 'confirmed');
assert.equal(core.mapAnswers(partial, partial).status, 'ambiguous');
assert.deepEqual(core.mapAnswers(partial, partial).labels, ['B']);
const changedIds = { ...partial, options: partial.options.map((option, index) => ({ ...option, id: `new-${index}` })) };
assert.equal(core.mapAnswers(partial, changedIds).status, 'ambiguous');
const textOnly = core.normalizeRecord(repeatedMultiple({ correctOptionIds: [], answerTexts: ['下降'] }));
assert.equal(core.mapAnswers(textOnly, textOnly).status, 'ambiguous');
});
@@ -240,6 +251,68 @@ test('image upgrade refuses partial, ambiguous or semantically changed evidence'
});
});
function mathEntityFixture() {
const value = imageUpgradeFixture();
value.existing[0].stem = '传热速率=系数×推动力,允许偏差±2。';
value.incoming[0].stem = '传热速率=系数×推动力,允许偏差±2。';
return value;
}
test('complete source snapshot repairs only the evidenced times and plusmn spellings', () => {
const { existing, incoming, context } = mathEntityFixture();
const original = JSON.stringify([existing, incoming]);
assert.equal(core.mapAnswers(existing[0], incoming[0]).status, 'unmapped', 'ordinary comparisons must not decode literal entities');
const result = core.upgradeLegacyMathEntities(existing, incoming, context);
assert.equal(result.upgraded.length, 1);
assert.equal(result.upgraded[0].kind, 'math-entity');
assert.equal(result.existing.length, 1);
assert.ok(result.incoming[0].questionIdAliases.includes(existing[0].questionId));
assert.equal(JSON.stringify([existing, incoming]), original);
assert.equal(core.compactRecords(core.upsert(result.existing, result.incoming)).records.length, 2);
const alreadyDuplicated = core.upgradeLegacyMathEntities([...existing, incoming[0]], incoming, context);
assert.equal(alreadyDuplicated.upgraded.length, 1, 'a previous failed migration containing both representations must recover');
assert.equal(core.compactRecords(core.upsert(alreadyDuplicated.existing, alreadyDuplicated.incoming)).records.length, 2);
const sameId = mathEntityFixture(); sameId.incoming[0].questionId = sameId.existing[0].questionId;
assert.equal(core.upgradeLegacyMathEntities(sameId.existing, sameId.incoming, context).upgraded.length, 1);
const options = imageUpgradeFixture();
options.incoming[0].stem = options.existing[0].stem;
options.existing[0].options[0].text = '12+1×2×2';
options.incoming[0].options[0].text = '12+1×2×2';
assert.equal(core.upgradeLegacyMathEntities(options.existing, options.incoming, context).upgraded.length, 1, 'non-correct option text can be completed without changing the standard answer');
});
test('math entity completion retains literal entities, missing positions and real differences', async t => {
const cases = [
['incomplete snapshot', value => { value.incoming.pop(); }],
['unverified page count', value => { value.context.expectedCount = 3; }],
['not a full snapshot', value => { value.context.fullSnapshot = false; }],
['wrong bank', value => { value.context.bankId = 'other-bank'; }],
['old position missing', value => { delete value.existing[0].questionIndex; }],
['different position', value => { value.existing[0].questionIndex = 1; }],
['new positions overlap', value => { value.incoming[1].questionIndex = 0; }],
['different option ID identity', value => { value.incoming[0].options[0].id = 'replacement-id'; }],
['different standard answer', value => { value.incoming[0].correctOptionIds = ['x']; value.incoming[0].answerTexts = ['降低温度']; }],
['unknown entity outside the whitelist', value => { value.existing[0].stem = '系数÷推动力'; value.incoming[0].stem = '系数÷推动力'; }],
['literal entity remains in the new question', value => { value.incoming[0].stem = value.existing[0].stem; }],
['part of the entity text intentionally remains literal', value => { value.incoming[0].stem = '传热速率=系数×推动力,允许偏差±2。'; }],
['different operator', value => { value.incoming[0].stem = '传热速率=系数x推动力,允许偏差±2。'; }],
['different image content', value => { value.existing[0].stem += '[图片:https://example.test/a.png]'; value.incoming[0].stem += '[图片:https://example.test/b.png]'; }],
['same legacy spelling occurs twice', value => { value.existing.push({ ...value.existing[0], questionId: 'second-literal', questionIndex: 4 }); }],
['separate modern question at a different position', value => { value.existing.push({ ...value.incoming[0], questionIndex: 4 }); }],
['a true second question intentionally uses literal spelling', value => { value.incoming[1] = { ...value.existing[0], questionId: 'literal-second', questionIndex: 1, source: value.incoming[1].source }; }],
['old source is not a network list', value => { value.existing[0].source.kind = 'dom'; }],
['mixed incoming batches', value => { value.incoming[1].source.batchId = 'different'; }],
['same batch is not evidence of an upgrade', value => { value.existing[0].source.batchId = value.incoming[0].source.batchId; }]
];
for (const [name, mutate] of cases) await t.test(name, () => {
const value = mathEntityFixture(); mutate(value);
const result = core.upgradeLegacyMathEntities(value.existing, value.incoming, value.context);
assert.equal(result.upgraded.length, 0);
assert.equal(result.existing.length, value.existing.length);
});
});
test('same stem but different option sets never auto-map', () => {
const record = core.normalizeRecord(fixture());
const current = reorder(record);