Files
Pouya LajevardiandClaude Opus 5 3c3ba5dc6e
Build and deploy / build-and-deploy (push) Failing after 4s
feat: price med-arb by phase, attest the conflicts undertaking, and answer the first real spam
Pouya's rulings of 2026-09-03 (the last two D20 findings) and 2026-09-04 (the
spam observation and four mitigations), in one change set.

D20 finding 10 — med-arb is billed BY PHASE, each phase at the rates already
published, so /fees/'s "Every figure is on this page" is true as written rather
than narrowed. FEES.medArb is the single source; docs/07 §Med-arb carries the
rule INTERIM against R5, and R5 now carries it back, because a derived price
moves silently when a rate moves.

D20 finding 13 — conduct undertaking (g), attested 2026-09-03, published as his
wording verbatim on /legal/privacy/ and /contact/. The clause that raised the
finding promised to DISCLOSE a conflicts check's outcome, which the attestation
does not cover; it is struck. D20 now partitions 17 fixed / 2 refuted / 1 owed.

Spam, 2026-09-04 — recorded in docs/05 §Observed abuse with the date and
signature. A second honeypot (a decoy checkbox, own class, `hidden`, a label
that tells a human not to tick it) and scoring that LABELS and never rejects:
nothing is dropped, nothing new is stored, and only the operator notification
changes. Q65 opens the WAF cost call.

The timing floor could not be built: there is no timing check and never has
been. docs/05 carries it struck, and every mechanism that would give a real
per-visitor clock breaks zero-JS, handler-and-form-only, or D1. Q66.

configure.mjs gains section 5 — a custom origin request policy forwarding
CloudFront-Viewer-Address on /api/*. Written, dry-run against the live
distribution, NOT applied. It reads the handler's own header reads and refuses
to run if the whitelist omits one.

And reading the live account to do it found four AGENTS.md §7 rows saying the
intake backend was undeployed, two days after it went live — corrected against
get-function-configuration, get-routes, get-stage, get-policy and the deployed
zip, which was downloaded and read.

Review: adversarial-reviewer only (claims-auditor is D20's cutover pass and has
run). Round 1 five lenses, 56 findings, 7 blocking, 4 refuted by an independent
refuter; round 2 four lenses, 36 findings, 33 of them defects in round 1's own
repairs. Stopped at two per D19.

Gates, exit status read for each: check 0 · build 0 (23 pages) · check:claims 0
· check:intake 0 · og:proof 0 · lint 0 · spam-score.test 39/39 with 6/6 mutations
killed · router.test 30/30 · minifier grep 1 (clean) · lighthouse 0, no category
below 95 · configure.mjs dry run 0, nothing written.

Nothing deployed and nothing applied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Md3GndFqWPzK78xAoebsg5
2026-09-04 10:06:37 -04:00

197 lines
9.8 KiB
JavaScript

/**
* Spam SCORING for the intake handler. Pouya's ruling, 2026-09-04.
*
* ⚠️ **THIS MODULE NEVER REJECTS ANYTHING, AND THAT IS THE WHOLE DESIGN.** It
* returns a score and a list of signal names. The handler stores the record and
* sends both emails either way; above the threshold it prefixes the OPERATOR
* notification's subject with `[Possible spam] ` and adds one line naming the
* signals. Pouya filters in Gmail. His words: *"Nothing is dropped; a false
* positive costs him one glance."*
*
* That asymmetry is why the thresholds below can be tuned aggressively. The cost
* of a false positive is a subject-line prefix; the cost of a false negative is
* one unlabelled email. Neither loses an inquiry — which a filter that rejected
* would, and a legal inquiry lost silently is the one outcome this form must not
* produce.
*
* ⚠️ **NOTHING HERE IS STORED.** The score and the signals do not enter the
* DynamoDB item. `/legal/privacy/` publishes what the record holds, field by
* field, and adding an attribute would make that list wrong — a disclosure
* defect, not a schema change. The label lives only in the operator
* notification. ⚠️ **THAT MAILBOX IS DELEGATED, NOT PERSONAL** — §9 Q63 and
* `/legal/privacy/` §Who can see it both say so, and an earlier draft of this
* comment said the label "lives in an email that only Pouya reads", which is the
* exclusivity Q63 struck. It reaches whoever reads `info@smlcompany.ca`. If a
* stored score is ever wanted, the page changes first.
*
* ⚠️ **AND THE INQUIRER NEVER SEES ANY OF THIS.** The confirmation email is
* untouched. A person wrongly scored must not be told a machine thought they
* were a bot.
*
* WHY SCORING RATHER THAN MORE REJECTION. The two submissions of 2026-09-04
* (`docs/05` §Observed abuse) passed the honeypot. Every rule that would have
* caught them — a foreign phone, a link in the summary, a disposable-looking
* address — is a rule some real inquirer also trips: this practice takes
* cross-border commercial work, so a `+44` number is a client, not a bot. A
* rejecting rule set built from those signals would eventually discard a real
* dispute and report success while doing it.
*/
/**
* ⚠️ **NOT MEASURED FROM A CORPUS — THERE IS NO CORPUS.** No genuine inquiry has
* arrived through this form yet, so there is nothing to measure a normal summary
* length against, and a number presented as measured when it is not is the
* defect `AGENTS.md` keeps paying for.
*
* It is DERIVED, and the derivation is the form's own instruction: the `summary`
* field's hint reads *"A few sentences is enough."* This floor sits **below** what
* that invites, so it fires on a summary that does not attempt the question
* rather than on one that answers it briefly. `[assumed 2026-09-04]`
*
* ⚠️ **TUNE IT DOWN WHEN IN DOUBT, NEVER UP.** An unlabelled spam costs nothing
* that matters; a labelled real inquiry spends the reader's trust in the label.
* *"Shareholder dispute, two directors, Ontario CBCA company."* is 57 characters
* and is exactly what the hint asks for — a floor above that scores the form's
* own instruction as a spam signal.
*
* **Pouya can replace this with a measurement whenever he likes** — the two spam
* records of 2026-09-04 are still in the table, and their `summary` lengths are
* the first real data this number could rest on. §9 Q65 records that.
*/
export const SHORT_SUMMARY_CHARS = 100;
/**
* Above this, the notification is labelled. Weights below are 1 for a signal a
* real inquirer plausibly trips and 2 for one they rarely do, so the threshold
* of 2 means: **one strong signal, or two weak ones.**
*
* Worked, because a threshold nobody has worked through is a guess with a number
* on it:
* - Ontario counsel, local number, three-line summary → 0, clean
* - Cross-border counsel, `+44` number, three-line summary → 1, clean
* - Cross-border counsel, `+44` number, one-line summary → 2, LABELLED
* - A four-part real name at gmail.com → 1, clean
* - Anyone pasting a link to a public tender document → 2, LABELLED
* - foreign number + a short scraped summary carrying a link → 4, LABELLED
*
* The third and fourth rows are the accepted false positives. Both are real
* shapes, both cost one glance, and both were preferred to missing the fifth.
*
* ⚠️ **THE LAST ROW IS A SHAPE, NOT A MEASUREMENT OF THE TWO 2026-09-04
* SUBMISSIONS. THOSE RECORDS WERE NEVER READ.** What the attested signature
* guarantees is a non-NANP phone — **one weak signal** — and whether either is
* labelled turns on facts only the two rows in the table hold. §9 Q65 records
* that they are still there and are the only real data any of these numbers
* could rest on.
*/
export const SPAM_THRESHOLD = 2;
/** `https://…` or `www.…` only. A bare `acme.com` is NOT matched: an inquirer
* writing "the dispute concerns acme.com's supply contract" is describing a
* party, and matching that would label ordinary commercial prose. */
const URL_IN_TEXT = /\b(?:https?:\/\/|www\.)\S/i;
/**
* NANP: an explicit `+<cc>` settles it; otherwise ten digits, or eleven
* beginning with 1, after the tail is dropped.
*
* ⚠️ **A DIGIT COUNT ALONE CANNOT DO THIS.** `416-555-0123 ext 22` is twelve
* digits, `416-555-0123 or 416-555-0124` is twenty, and both are ordinary
* Toronto numbers that a bare count calls foreign — a signal saying the opposite
* of the truth. The tail is dropped at the first extension marker or
* second-number separator, and the marker list is deliberately generous.
*
* **The country code is read FIRST because it is the only unambiguous thing in
* the field.** `+44 …` and `+7 …` are settled without counting anything, which
* is what a pure shape test cannot do: `+7 912 345 6789` is grouped 3-3-4
* exactly like a NANP number, so matching the shape would call it Canadian.
* Only when there is no explicit country code does the digit count run, and then
* the tail is dropped at the first extension marker or second-number separator.
*/
function looksNorthAmerican(phone) {
const trimmed = phone.trim();
/* An explicit international prefix is decisive in both directions. */
const cc = trimmed.match(/^\+\s*(\d{1,3})/);
if (cc) return cc[1] === '1';
/* Longest alternative FIRST: regex alternation is leftmost-first, so `ext`
placed before `extension` matches the first three letters and then relies on
backtracking. Ordering it correctly is cheaper than depending on that.
`\bx\b` would NOT match the `x` in `x22` — the digit after it is a word
character, so there is no boundary — which is how `(416) 555-0123 x22` scored
foreign for one round. The marker is matched by what FOLLOWS it. */
const digits = trimmed
.split(/\s*(?:extension|extn|ext|x)[.:-]?\s*\d|[#,;]|\bor\b/i)[0]
.replace(/\D/g, '');
return digits.length === 10 || (digits.length === 11 && digits[0] === '1');
}
/**
* The Gmail dot trick: one mailbox, unlimited distinct-looking addresses,
* because Gmail ignores dots in the local part.
*
* ⚠️ **A DOT IS NOT THE SIGNAL, AND TREATING IT AS ONE WOULD LABEL MOST REAL
* GMAIL USERS.** `first.last@gmail.com` is the single most ordinary form a Gmail
* address takes. What distinguishes the trick is dot DENSITY: **three or more
* dots**, and nothing else.
*
* ⚠️ **AND THE WEIGHT IS 1, NOT 2, WHICH MATTERS MORE THAN THE BOUNDARY DOES.**
* `mary.jane.o.brien@gmail.com` and `maria.de.la.cruz@gmail.com` are three-dot
* REAL names — compound surnames and middle initials are ordinary, not rare —
* and weight 2 is defined here as what a real inquirer rarely trips. At weight 1
* nothing can be labelled on the shape of its owner's name alone; a genuine
* dot-trick address reaches the threshold as soon as it trips anything else,
* which spam reliably does. **Do not raise it back.**
*/
function looksLikeGmailDotTrick(email) {
const at = email.lastIndexOf('@');
if (at < 1) return false;
const local = email.slice(0, at);
const domain = email.slice(at + 1).toLowerCase();
if (domain !== 'gmail.com' && domain !== 'googlemail.com') return false;
const dots = local.split('.').length - 1;
return dots >= 3;
}
/**
* @param {Record<string, string>} fields the handler's `clean` map — validated,
* plain-texted values, keyed by field name. Absent fields are simply absent.
* @returns {{score: number, signals: string[]}} `signals` are written for a
* human reading one line of an email, not for a machine.
*/
export function scoreSubmission(fields) {
const signals = [];
let score = 0;
const add = (weight, label) => {
score += weight;
signals.push(label);
};
const summary = fields.summary ?? '';
const phone = fields.phone ?? '';
const email = fields.email ?? '';
/* Only when a summary exists. An absent one is a validation failure the
handler has already turned into the failure page, so scoring an empty
string here would be scoring a submission that never got this far. */
if (summary !== '' && summary.length < SHORT_SUMMARY_CHARS) {
add(1, `summary under ${SHORT_SUMMARY_CHARS} characters`);
}
/* `phone` is OPTIONAL. Not giving one is not a signal — most inquirers will
not — so this fires only on a number that is present and not North
American. Treating absence as suspicious would label the quiet majority. */
if (phone !== '' && !looksNorthAmerican(phone)) {
add(1, 'phone is not a Canadian or US number');
}
if (URL_IN_TEXT.test(summary)) {
add(2, 'link in the dispute summary');
}
if (looksLikeGmailDotTrick(email)) {
add(1, 'Gmail address using the dot trick');
}
return { score, signals };
}
/** True when the operator notification should carry the label. */
export const isPossibleSpam = ({ score }) => score >= SPAM_THRESHOLD;