diff --git a/frontend/index.html b/frontend/index.html
index d74ef35..3ddd1f8 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -7642,7 +7642,10 @@ async function profilerRunRule(r) {
const n = Math.min(exp.length, act.length);
if (exp.length !== act.length) {
res.status = 'fail';
- res.checks.push({ label: 'content.records', expected: exp.length + ' records', actual: act.length + ' records', ok: false });
+ const numRec = res.checks.find(c => c.label === 'numRecords');
+ if (!numRec || numRec.ok) {
+ res.checks.push({ label: 'content.records', expected: exp.length + ' records', actual: act.length + ' records', ok: false });
+ }
}
const matched = [];
for (let j = 0; j < n; j++) {
diff --git a/frontend/sw.js b/frontend/sw.js
index 93541b0..c21ade2 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'otaman-v58';
+const CACHE = 'otaman-v59';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/profiler.test.js b/frontend/tests/profiler.test.js
index 6388e66..29a3fbb 100644
--- a/frontend/tests/profiler.test.js
+++ b/frontend/tests/profiler.test.js
@@ -474,6 +474,46 @@ test('profilerRunRule records which records matched on a record mismatch', async
assert.deepStrictEqual(res.recordsMatched, [1, 3]);
});
+const REC_SELECT_10 = { name: 'EF.X', fid: '6F3A', file_type: 'linear_fixed', file_size: null, record_len: 2, num_of_rec: 10, exists: true };
+const REC_EXP_30 = (() => { const r = []; for (let i = 1; i <= 30; i++) r.push({ num: i, data: 'AB' }); return r; })();
+const REC_ACT_10 = (() => { const r = []; for (let i = 1; i <= 10; i++) r.push({ num: i, data: 'AB' }); return r; })();
+
+test('record count mismatch is reported once when numRecords is checked', async () => {
+ mockFetch({ '/api/select': () => REC_SELECT_10, '/api/read': () => ({ success: true, records: REC_ACT_10 }) });
+ const res = await profilerRunRule({
+ path: 'MF/7F20/6F3A', fileType: 'linear_fixed', recordLen: 2, numRecords: 30, fciMode: 'type_size',
+ content: { mode: 'exact', kind: 'record', records: REC_EXP_30 },
+ });
+ assert.strictEqual(res.status, 'fail');
+ assert.ok(res.checks.some(c => c.label === 'numRecords' && c.ok === false));
+ assert.ok(!res.checks.some(c => c.label === 'content.records'));
+});
+
+test('record count mismatch keeps content.records when numRecords is not checked (type mode)', async () => {
+ mockFetch({ '/api/select': () => REC_SELECT_10, '/api/read': () => ({ success: true, records: REC_ACT_10 }) });
+ const res = await profilerRunRule({
+ path: 'MF/7F20/6F3A', fileType: 'linear_fixed', recordLen: 2, numRecords: 30, fciMode: 'type',
+ content: { mode: 'exact', kind: 'record', records: REC_EXP_30 },
+ });
+ assert.strictEqual(res.status, 'fail');
+ assert.ok(res.checks.some(c => c.label === 'content.records' && c.ok === false));
+ assert.ok(!res.checks.some(c => c.label === 'numRecords'));
+});
+
+test('record count mismatch keeps content.records when numRecords passes but read differs', async () => {
+ mockFetch({
+ '/api/select': () => ({ ...REC_SELECT_10, num_of_rec: 3 }),
+ '/api/read': () => ({ success: true, records: REC_ACT_10.slice(0, 2) }),
+ });
+ const res = await profilerRunRule({
+ path: 'MF/7F20/6F3A', fileType: 'linear_fixed', recordLen: 2, numRecords: 3, fciMode: 'type_size',
+ content: { mode: 'exact', kind: 'record', records: [{ num: 1, data: 'AB' }, { num: 2, data: 'AB' }, { num: 3, data: 'AB' }] },
+ });
+ assert.strictEqual(res.status, 'fail');
+ assert.ok(res.checks.some(c => c.label === 'numRecords' && c.ok === true));
+ assert.ok(res.checks.some(c => c.label === 'content.records' && c.ok === false));
+});
+
test('profilerRenderReport includes the checked-aspects summary and matching-record note', () => {
global.t = s => s;
global.pysimCustomFiles = [];