openvibers/openvibe.rent Commits

Rent: the listing screen shows only signs a quote from the listing backs (#51)

OP OpenVibers committed 29 days ago · 74a254dd11 · 2 files, +40 −14

The listing screen's first production run (Rent#50, local model) named all six signs for a scam text that showed three: owner abroad, deposit before viewing, wire transfer. Staff would have read signs the listing doesn't show.

`readOutput` now grounds the answer in the listing it read:

- A quote counts only when its words are in the listing's title or description (case, curly quotes and spacing normalised). An invented or paraphrased quote is dropped.
- A sign counts only when a grounded quote is labelled with it. A sign that is named but has no quote behind it never reaches staff.
- With no grounded sign the answer is `allow`.

`test/screen.test.js`: the stand-in names five signs plus an unknown one and quotes one invented sentence. Exactly the three real signs reach the page with their three quotes. The unit checks cover invented quotes, case and spacing, and the caps.

Changes

ddiff --git a/server/listings/screen.js b/server/listings/screen.js
iindex 37a8498..b0969de 100644
---- a/server/listings/screen.js
++++ b/server/listings/screen.js
@@@ -46,14 +46,25 @@ function screenInput(row) {
};
}
--/** Bound and validate the workflow's output: unknown signs are dropped; `review` without a sign is `allow`. */
--function readOutput(output) {
++const norm = (t) => String(t == null ? '' : t).toLowerCase().replace(/[\u2018\u2019]/g, "'").replace(/[\u201c\u201d]/g, '"').replace(/\s+/g, ' ').trim();
++
++/**
++ * Bound, validate and ground the workflow's output in the listing it read. A quote counts only when its words really
++ * are in the listing's title or description (a model may paraphrase or invent one), and a sign counts only when a
++ * grounded quote is labelled with it: staff never read a sign the text does not show. (On its first production run
++ * the local model named all six signs for a text that showed three.) With no grounded sign the answer is allow.
++ */
++function readOutput(output, row = null) {
const o = output && typeof output === 'object' ? output : {};
-- const signals = [...new Set((Array.isArray(o.signals) ? o.signals : []).filter((x) => SIGNALS.includes(x)))];
-- const action = o.action === 'review' && signals.length ? 'review' : 'allow';
-- const spans = (Array.isArray(o.spans) ? o.spans : []).slice(0, 10)
-- .filter((x) => x && typeof x.quote === 'string' && x.quote.trim())
++ const text = row ? norm(`${row.title || ''}\n${row.description || ''}`) : null;
++ const spans = (Array.isArray(o.spans) ? o.spans : [])
++ .filter((x) => x && typeof x.quote === 'string' && norm(x.quote))
++ .filter((x) => !text || text.includes(norm(x.quote)))
++ .slice(0, 10)
.map((x) => ({ quote: x.quote.trim().slice(0, 300), signal: SIGNALS.includes(x.signal) ? x.signal : null }));
++ const backed = new Set(spans.map((x) => x.signal).filter(Boolean));
++ const signals = SIGNALS.filter((x) => backed.has(x));
++ const action = o.action === 'review' && signals.length ? 'review' : 'allow';
const rationale = typeof o.rationale === 'string' ? o.rationale.trim().slice(0, 1000) : '';
return { action, signals: action === 'review' ? signals : [], spans: action === 'review' ? spans : [], rationale };
}
@@@ -86,7 +97,7 @@ function createScreen({ s, ai = null, log = console }) {
log.warn(`[Screen] listing ${id}: ${(err && err.message) || err}`);
return 'failed';
}
-- const out = readOutput(run && run.output);
++ const out = readOutput(run && run.output, row);
await store.putCheck(s, { listingId: row.id, contentHash: hash, ...out, runId: (run && run.runId) || null });
if (out.action === 'review') log.log(`[Screen] listing ${row.id} flagged for staff: ${out.signals.join(', ')}`);
return out.action;
ddiff --git a/test/screen.test.js b/test/screen.test.js
iindex 6ce1d3a..16aa2b5 100644
---- a/test/screen.test.js
++++ b/test/screen.test.js
@@@ -25,7 +25,18 @@ const SCAM = 'I am abroad for work so I cannot show it. Send the deposit by wire
return {
runId: `run_${calls.length}`,
output: scam
-- ? { action: 'review', signals: ['owner_unavailable', 'payment_before_viewing', 'unusual_payment', 'made_up'], rationale: 'Pay by wire before any viewing; the owner is away.', spans: [{ quote: 'Send the deposit by wire transfer today', signal: 'payment_before_viewing' }] }
++ ? {
++ action: 'review', rationale: 'Pay by wire before any viewing; the owner is away.',
++ // Named but unbacked (pressure, personal_data_request), unknown (made_up), and one invented quote:
++ // only the signs a quote from the listing stands behind reach staff.
++ signals: ['owner_unavailable', 'payment_before_viewing', 'unusual_payment', 'pressure', 'personal_data_request', 'made_up'],
++ spans: [
++ { quote: 'Send the deposit by wire transfer today', signal: 'payment_before_viewing' },
++ { quote: 'by wire transfer', signal: 'unusual_payment' },
++ { quote: 'I am abroad for work so I cannot show it.', signal: 'owner_unavailable' },
++ { quote: 'Pay in bitcoin within the hour', signal: 'pressure' },
++ ],
++ }
: { action: 'allow', signals: ['none'], rationale: '', spans: [] },
};
},
@@@ -59,7 +70,8 @@ const SCAM = 'I am abroad for work so I cannot show it. Send the deposit by wire
await t.ctx.screen.settle();
const c = await store.getCheck(s, scam.id);
assert.strictEqual(c.action, 'review');
-- assert.deepStrictEqual(c.signals, ['owner_unavailable', 'payment_before_viewing', 'unusual_payment'], 'an unknown sign is dropped');
++ assert.deepStrictEqual(c.signals, ['payment_before_viewing', 'unusual_payment', 'owner_unavailable'], 'only signs a real quote backs: unknown, unbacked and invented ones are dropped');
++ assert.strictEqual(c.spans.length, 3, 'the invented quote is gone');
assert.strictEqual(await store.countFlagged(s), 1);
assert.strictEqual((await t.get(`/api/v1/listings/${scam.id}`)).status, 200, 'still public: a flag only asks staff to look');
const page = await t.get('/staff?state=flagged', { as: ada });
@@@ -110,13 +122,16 @@ const SCAM = 'I am abroad for work so I cannot show it. Send the deposit by wire
assert.strictEqual((await store.getCheck(s, late.id)).action, 'allow');
});
-- await check('the output is bounded: review without a known sign is allow; quotes are trimmed and capped', () => {
-- assert.deepStrictEqual(readOutput({ action: 'review', signals: ['none'] }), { action: 'allow', signals: [], spans: [], rationale: '' });
-- assert.strictEqual(readOutput(null).action, 'allow');
-- const many = readOutput({ action: 'review', signals: ['pressure'], spans: Array.from({ length: 30 }, () => ({ quote: ` ${'x'.repeat(400)} `, signal: 'nope' })) });
++ await check('the output is grounded and bounded: a sign needs a quote from the listing; quotes are trimmed and capped', () => {
++ const row = { title: 'Room', description: `Act fast, ${'x'.repeat(400)} today only.` };
++ assert.deepStrictEqual(readOutput({ action: 'review', signals: ['none'] }, row), { action: 'allow', signals: [], spans: [], rationale: '' });
++ assert.strictEqual(readOutput(null, row).action, 'allow');
++ assert.strictEqual(readOutput({ action: 'review', signals: ['pressure'], spans: [{ quote: 'Wire it now', signal: 'pressure' }] }, row).action, 'allow', 'an invented quote backs nothing');
++ const real = readOutput({ action: 'review', signals: ['pressure'], spans: [{ quote: 'ACT FAST', signal: 'pressure' }, { quote: 'today only', signal: 'nope' }] }, row);
++ assert.deepStrictEqual([real.action, real.signals, real.spans.length, real.spans[1].signal], ['review', ['pressure'], 2, null], 'case and spacing do not matter');
++ const many = readOutput({ action: 'review', spans: Array.from({ length: 30 }, () => ({ quote: ` ${'x'.repeat(400)} `, signal: 'pressure' })) }, row);
assert.strictEqual(many.spans.length, 10);
assert.strictEqual(many.spans[0].quote.length, 300);
-- assert.strictEqual(many.spans[0].signal, null);
});
await check('without OpenVibe.AI nothing is screened and nothing starts', async () => {