feat: publish verified five-bank question dataset
This commit is contained in:
+70
-1
@@ -242,6 +242,62 @@ const QBCore = (() => {
|
||||
return { existing: existing.filter(record => !removed.has(record.key)), incoming, upgraded };
|
||||
}
|
||||
|
||||
// These are deliberately evidenced legacy spellings, not an HTML decoder.
|
||||
// Literal entities elsewhere stay distinct in normalization/search/compaction.
|
||||
function upgradeLegacyMathEntities(existingInput, incomingInput, { bankId = '', expectedCount, fullSnapshot = false } = {}) {
|
||||
if (!Array.isArray(existingInput) || !Array.isArray(incomingInput)) throw new Error('题库必须是数组');
|
||||
const existing = existingInput.map(normalizeRecord), incoming = incomingInput.map(normalizeRecord);
|
||||
const unchanged = () => ({ existing, incoming, upgraded: [] });
|
||||
if (!fullSnapshot || !bankId || !Number.isSafeInteger(expectedCount) || expectedCount < 1 || incoming.length !== expectedCount) return unchanged();
|
||||
const isList = source => source?.kind === 'network' && /\/(?:upm\/(?:wrong\/)?start|pra\/[^/]+\/questions)\/?$/u.test(source.url || '') && Number.isFinite(Date.parse(source.observedAt));
|
||||
const batch = incoming[0]?.source.batchId;
|
||||
if (!batch || incoming.some(record => record.bankId !== bankId || !isList(record.source) || record.source.batchId !== batch ||
|
||||
!Number.isSafeInteger(record.questionIndex) || record.questionIndex < 0 || record.questionIndex >= expectedCount || !record.questionId) ||
|
||||
new Set(incoming.map(record => record.questionIndex)).size !== expectedCount || new Set(incoming.map(record => record.questionId)).size !== expectedCount) return unchanged();
|
||||
const content = record => [record.stem, ...record.options.map(option => option.text)];
|
||||
const entities = { '×': '×', '±': '±' };
|
||||
const hasEntity = record => content(record).some(value => /&(?:times|plusmn);/.test(value));
|
||||
const canonical = value => normalized(value.replace(/&(?:times|plusmn);/g, entity => entities[entity]));
|
||||
const signature = record => JSON.stringify([record.type, canonical(record.stem), sorted(record.options.map(option => canonical(option.text)))]);
|
||||
const optionPairs = record => record.options.map(option => JSON.stringify([option.id, canonical(option.text)]));
|
||||
const correctContents = record => {
|
||||
if (record.answerStatus !== 'confirmed' || record.conflicts.length || !record.correctOptionIds.length || record.options.length < 2 ||
|
||||
record.options.some(option => !option.id) || new Set(record.options.map(option => option.id)).size !== record.options.length ||
|
||||
new Set(record.options.map(option => canonical(option.text))).size !== record.options.length) return null;
|
||||
const selected = record.correctOptionIds.map(id => record.options.find(option => option.id === id));
|
||||
return selected.some(option => !option) ? null : selected.map(option => canonical(option.text));
|
||||
};
|
||||
const sameAnswers = (a, b) => {
|
||||
const left = correctContents(a), right = correctContents(b);
|
||||
return left && right && sameArray(left, right) && sameArray(a.correctOptionIds, b.correctOptionIds) &&
|
||||
sameArray(unique(a.answerTexts.map(canonical)), unique(b.answerTexts.map(canonical)));
|
||||
};
|
||||
const sameModern = (record, current) => !hasEntity(record) && record.type === current.type && record.questionIndex === current.questionIndex &&
|
||||
normalized(record.stem) === normalized(current.stem) && sameArray(optionPairs(record), optionPairs(current)) && isList(record.source) && sameAnswers(record, current);
|
||||
const bank = existing.filter(record => record.bankId === bankId), upgraded = [], removed = new Set();
|
||||
for (let index = 0; index < incoming.length; index++) {
|
||||
const current = incoming[index], key = signature(current);
|
||||
if (hasEntity(current) || current.type === 'unknown' || !correctContents(current) || incoming.filter(record => signature(record) === key).length !== 1) continue;
|
||||
const candidates = bank.filter(record => hasEntity(record) && signature(record) === key);
|
||||
if (candidates.length !== 1) continue;
|
||||
const previous = candidates[0];
|
||||
if (!previous.questionId || previous.questionIndex !== current.questionIndex || !Number.isSafeInteger(previous.questionIndex) ||
|
||||
!isList(previous.source) || (previous.source.batchId ? previous.source.batchId === batch : previous.source.observedAt === current.source.observedAt) ||
|
||||
!sameArray(optionPairs(previous), optionPairs(current)) || !sameAnswers(previous, current)) continue;
|
||||
// A previous failed export may already contain both spellings. Permit only
|
||||
// fully agreeing modern copies at that very same source position; distinct
|
||||
// literal-entity questions and duplicate occurrences remain untouched.
|
||||
if (bank.some(record => (signature(record) === key || record.questionIndex === current.questionIndex) &&
|
||||
record.key !== previous.key && !sameModern(record, current))) continue;
|
||||
const replacement = normalizeRecord({ ...current, questionIdAliases: unique([...(current.questionIdAliases || []), previous.questionId, ...(previous.questionIdAliases || [])]) });
|
||||
incoming[index] = replacement;
|
||||
removed.add(previous.key);
|
||||
upgraded.push({ original: existingInput[existing.findIndex(record => record.key === previous.key)], replacement,
|
||||
kind: 'math-entity', bankId, expectedCount, batchId: batch, questionIndex: current.questionIndex });
|
||||
}
|
||||
return { existing: existing.filter(record => !removed.has(record.key)), incoming, upgraded };
|
||||
}
|
||||
|
||||
function conflictSnapshot(record, reason) {
|
||||
return { reason, stem: record.stem, options: record.options.map((option) => ({ ...option })), answerTexts: [...record.answerTexts], correctOptionIds: [...record.correctOptionIds], source: record.answerSource || record.source };
|
||||
}
|
||||
@@ -549,6 +605,19 @@ const QBCore = (() => {
|
||||
if (normalizedStem(record.stem) !== normalizedStem(current.stem) ||
|
||||
(record.type !== 'unknown' && current.type !== 'unknown' && record.type !== current.type)) return result('unmapped');
|
||||
if (record.options.length && !sameArray(optionContents(record), current.options.map((option) => comparableAnswer(option.text, record.type)))) return result('unmapped');
|
||||
const completeIdentity = record.type === current.type && record.type !== 'unknown' && record.options.length > 0 &&
|
||||
record.options.length === current.options.length && record.correctOptionIds.length > 0 &&
|
||||
record.options.every(option => option.id) && current.options.every(option => option.id) &&
|
||||
new Set(record.options.map(option => option.id)).size === record.options.length &&
|
||||
new Set(current.options.map(option => option.id)).size === current.options.length &&
|
||||
sameArray(record.options.map(option => JSON.stringify([option.id, comparableAnswer(option.text, record.type)])),
|
||||
current.options.map(option => JSON.stringify([option.id, comparableAnswer(option.text, record.type)])));
|
||||
if (completeIdentity) {
|
||||
const selected = record.correctOptionIds.map(id => current.options.find(option => option.id === id));
|
||||
if (selected.some(option => !option)) return result('unmapped');
|
||||
if (unique(current.options.map(option => option.label)).length !== current.options.length) return result('ambiguous');
|
||||
return result('matched', selected.map(option => option.label));
|
||||
}
|
||||
const labels = [];
|
||||
for (const value of answerTexts) {
|
||||
const expected = comparableAnswer(value, record.type);
|
||||
@@ -679,7 +748,7 @@ const QBCore = (() => {
|
||||
return records;
|
||||
}
|
||||
|
||||
return { normalizeRecord, repairDuplicateOptionConflicts, upgradeLegacyImageRecords, mergeRecord, upsert, compactRecords, search, mapAnswers, toCSV, fromCSV };
|
||||
return { normalizeRecord, repairDuplicateOptionConflicts, upgradeLegacyImageRecords, upgradeLegacyMathEntities, mergeRecord, upsert, compactRecords, search, mapAnswers, toCSV, fromCSV };
|
||||
})();
|
||||
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = QBCore;
|
||||
|
||||
Reference in New Issue
Block a user