No log statement, analytics event, error-tracker call or third-party SDK call receives personal data. No AI engine is involved: the verdict is reproducible from the source alone.
Inputs
Non-test code files of the checkout (JavaScript, TypeScript, Python, PHP), analysed statically with the tokens, function units and calls of the static analysis kit. A sink is a call to console/logger/log/logging/Log::/print/error_log/var_dump, an error tracker (Sentry, Bugsnag, Rollbar, New Relic, Datadog, Honeybadger, LogRocket, FullStory…) or an analytics SDK (Segment/analytics, PostHog, Mixpanel, Amplitude, gtag, Facebook Pixel, Hotjar, Intercom, Heap, RudderStack, dataLayer…). The rules read the arguments of each call as tokens: variables, property accesses (with their receiver), object keys, getters, and the interpolations of template literals, Python f-strings and PHP double-quoted strings (with the label before an interpolation: `email=${x}`). String literals carry no personal data on their own. An identifier that COUNTS records is read as a count, not as the field its words name. Paths listed in excludes (test, spec and tmp files, __tests__, __mocks__, fixtures, test, tests and audit-scripts folders) are never analysed. Nothing of the checkout is executed.
Decision rule
Not applicable when the non-test code has no sink call. FAIL (HIGH, with the line of the argument) when a sink receives a strong personal-data field (e-mail, phone, names, date of birth, national id, address, financial, health, biometric, IP address, precise location) as a variable, a property, an object key, a getter, or a labelled interpolation, or a whole user, customer, profile, member, account or record (`user`, `$customer`, `session.user`, `user.toJSON()`, a spread of them) or the request body (`req.body`, `request.data`, `$request->all()`). A contextual field is MEDIUM (blocks only inside Extended suites): username, ip, user agent, device id, avatar, bio, age wherever they come from; name, city, country, location, address, timezone, locale, company, job title, notes only when read from a person-like object (user, profile, customer, member, account, session, request body…). Category: pii-in-log for logs, pii-in-analytics for analytics SDKs, pii-to-third-party for error trackers. An argument is read as a COUNT of records and is not reported, and the trace says so, when the words of its name say it counts (a `without` before the field, as in `withoutVatNumber` or `intraCommunityWithoutVatNumber`; a head count/num/number of/total/sum/qty/rows/records/items/entries/lines/n, as in `rowsWithoutVatNumber`; a tail count/length/size/total/sum/qty/len/n, as in `emailsCount`) or when the file itself declares the name numeric (`x: number`, `= xs.filter(...).length`, `= count(rows)`, `= 0`, `n += 1`, `x > 0`); `number` is never a count tail, so `vatNumber`, `cardNumber`, `passportNumber`, `taxId`, `iban` and `holderName` keep failing, also when they arrive through a property path. A finding is cancelled, and the trace says so, when the field is redacted in the same call (a call named mask/redact/hash/omit/sanitize/scrub/anonymise/truncate/encrypt… wrapping the field or naming it or its object), in the enclosing function before the sink (a redaction call naming the field or its object), or by the logger configuration (pino/winston `redact` paths naming the field). PASS when every sink call was inspected and nothing blocking remains. Not judged by this check (declared): values whose content is only known at run time (error objects, generic data/result/payload/response variables, spreads of objects not named like a record), a record read from a receiver that is not a person, a request or a container (config.aws.profile, subscription.user), a record used as a condition (!user, user ? a : b), formatters and serialisers configured outside the call, breadcrumbs and contexts built elsewhere, and whether the third party's processor agreement covers the identifiers it receives.
Type
deterministic
1.1.1: false positive of the sweep of platform/projects/b2b (2026-09-24), fixed in the script: an identifier that COUNTS records is not the datum it counts. An argument is read as a count, and the trace says so, when the words of its name say it counts (a `without` before the field, as in withoutVatNumber or intraCommunityWithoutVatNumber; a head count/num/number of/total/sum/qty/rows/records/items/entries/lines/n, as in rowsWithoutVatNumber; a tail count/length/size/total/sum/qty/len/n, as in emailsCount) or when the file itself declares the name numeric (`x: number`, `= xs.filter(...).length`, `= count(rows)`, `= 0`, `n += 1`, `x > 0`). `number` is never a count tail, so vatNumber, cardNumber, passportNumber, taxId, iban and holderName keep failing, also through a property path (`${obj.vatNumber}`). (1.1.0: deterministic: the rule runs over the static analysis kit (tokens, function units, calls, imports, routes) with no AI engine; exact decisionRule and languages published; not-applicable with the reason when the checkout gives nothing to evaluate. The unit is the sink CALL and its arguments are read as tokens (variables, properties with receiver, object keys, getters, interpolations of template literals, f-strings and PHP strings with their label); string literals never count; redaction in the same call, in the enclosing function or in pino redact config cancels the finding (traced).)
// unit = a CALL to a sink (console/logger/logging/Log::/print · Sentry/Bugsnag/Rollbar/New Relic/Datadog · analytics/posthog/mixpanel/amplitude/gtag/fbq/dataLayer); its arguments are read as tokens
const WHOLE_RECORD_NAME = /^\$?(?:user|customer|profile|member|account|record|me|current_?user)$/i; // plus req.body, request.data, $request->all(), user.toJSON()
// variable / property / object key / getter / labelled interpolation `email=${x}` naming a strong field -> HIGH; whole record -> HIGH; contextual (username, ip, user agent; name/city on a person-like receiver) -> MEDIUM
// an identifier that COUNTS is not the datum it counts: `without` before the field, a count head (rows/count/num/total...), a count tail (...Count/...Length), or a name the file declares numeric (`: number`, `= xs.filter(...).length`, `> 0`) -> not reported (traced); `number` is never a count tail, so `${invoice.vatNumber}` still fails
// string literals alone never count; mask/redact/hash/omit/sanitize in the call, in the function before the sink, or pino `redact` paths -> cancelled (traced); no sink call -> not-applicable
The full script is disclosed on request in a read-only viewer (never published on GitHub); the attestation binds to this exact hash.