Every store holding personal data has an automatic enforcement of its retention period that names the store (TTL index, database retention policy, storage lifecycle rule, Laravel Prunable with model:prune scheduled, scheduled purge job that deletes or anonymises by age, or the account-deletion flow), and the enforced period matches the declared one; a policy document alone never counts. 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 (tokens, function units, calls, imports, routes) plus Prisma, SQL, YAML, TOML, Terraform, JSON, Markdown and text files and crontabs. Schema files (models, entities, migrations, Prisma, SQL) yield the personal-data stores as store blocks with their fields. Mechanisms: a TTL inside the store block (expireAfterSeconds, expires, TimeToLive, Prunable); a TTL, lifecycle or retention declaration (DynamoDB TimeToLiveSpecification, TimescaleDB add_retention_policy, pg_partman, DROP PARTITION, S3/GCS/Azure lifecycle, ExpirationInDays, retention_in_days, log retention, Redis EX/setex) whose 30 surrounding lines name the store; a scheduled unit (callbacks of cron/node-schedule/agenda/BullMQ registrations or identifiers passed to them, @Cron/@Interval/@Scheduled methods, Celery tasks named in a beat schedule, APScheduler jobs, Laravel $schedule->call/command/job, and scripts, modules or commands run by a Kubernetes CronJob, GitHub Actions schedule, EventBridge rule or crontab line) whose body or a function it calls within 2 module hops deletes, destroys, prunes or anonymises rows with a date comparison and names the store; the account-deletion flow (a unit named deleteAccount/deleteUser/closeAccount… or a DELETE /users|/accounts|/me route) deleting rows of the store within 2 module hops. Periods are read from the mechanism (seconds of expireAfterSeconds/expires/ttl, N * 86400, subDays(N), timedelta(days=N), interval 'N days', ExpirationInDays, retention_in_days…) and from documents (Markdown, text, YAML, TOML, JSON lines that name the store with retention wording and a number of days, weeks, months, years or hours). Paths listed in excludes are never analysed. Nothing of the checkout is executed.
Decision rule
Not applicable when no store with personal-data fields is declared. FAIL (HIGH, one finding per store with its schema line) when personal-data stores exist and the checkout has no enforcement mechanism of any kind. A store that no mechanism names (by its model, table or collection name, singular or plural, in the TTL declaration's surrounding lines, in the purge unit or its callees, or in the account-deletion flow) is MEDIUM (blocks in Extended suites). A scheduled unit named or worded as a retention, purge, cleanup or expiry job that deletes nothing by age in its body nor in the functions it calls within 2 module hops is MEDIUM (retention-job-inert); a Prunable model with model:prune scheduled nowhere is MEDIUM (retention-job-inert). When both the mechanism and a document declare a period for the same store and they differ by more than two days, MEDIUM (retention-mismatch) on the mechanism line. PASS when every store is named by at least one mechanism and no period mismatch exists; the summary names the mechanism per store (file, line, kind). Not judged by this check (declared): retention configured in infrastructure outside the checkout (a managed database policy, a bucket lifecycle set in a console, a scheduler defined elsewhere), whether the job actually runs in the deployed environment, whether the date column compared is the right one, and whether anonymisation is complete; a declared period without a period readable in the mechanism is logged, not compared.
Type
deterministic
1.1.1: a callee whose root is a name that `Object.prototype` also carries (`build().toString()` tokenizes to the bare callee `toString`; also `constructor`, `valueOf`, `hasOwnProperty`) was looked up in the plain object that holds the imported names, so it resolved to the INHERITED FUNCTION instead of to nothing and the whole check died with "name.includes is not a function". Only a real binding counts now, and the name of a unit is always read as a string. Found on a 1 319-file checkout (2fa-available and session-expiry-rotation, 2026-09-24); the same line was in the 15 scripts that walk the call graph, so all fifteen ship the fix. (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. Stores are schema STORE BLOCKS bound by NAME to a mechanism (TTL in the block, a TTL/lifecycle/retention declaration naming the store within 30 lines, a scheduled unit — cron, @Cron, Celery beat, APScheduler, Laravel schedule, CronJob/Actions/EventBridge manifests — whose call graph (2 module hops) deletes or anonymises by a date comparison naming the store, Prunable with model:prune scheduled, or the account-deletion flow); mechanism period vs declared period (MEDIUM on mismatch). Not judged: retention configured in infrastructure outside the checkout, whether the job runs in the deployed environment, the right date column, completeness of the anonymisation. (1.0.2: never analyses temporary files (*.tmp.*; spec.excludes follows AUDITOR_ANALYSIS_EXCLUDES). (1.0.1: never analyses test, spec, fixture and mock paths nor the auditor's own scripts (spec.excludes = AUDITOR_ANALYSIS_EXCLUDES); a fixture-looking secret (sk_test_, example, dummy) in real code is LOW, informative; every model call stays under the engine prompt cap (spec.promptMaxChars = AUDITOR_AI_PROMPT_MAX_CHARS): assess mode sends the candidate set in parts and aggregates the verdicts (a HIGH in any part is a FAIL, an unsatisfied part is a FAIL).)))
// stores = STORE BLOCKS of the schema files (Prisma model, Mongoose schema, Django/SQLAlchemy model, Laravel migration, SQL table) with personal-data fields
const TTL_DECLARATION = /TimeToLiveSpecification|add_retention_policy\(|pg_partman|DROP\s+PARTITION|lifecycle_rule|ExpirationInDays|retention_in_days|log_retention|setex\(/i; // names the store in its 30 surrounding lines
// scheduled unit (cron/@Cron/Celery beat/$schedule->/CronJob manifest) -> call graph <= 2 module hops -> a delete/destroy/prune/anonymise call + AGE_COMPARE naming the store
// no mechanism at all -> HIGH per store; store named by no mechanism -> MEDIUM; retention job without age delete -> MEDIUM; period of mechanism != declared period -> MEDIUM
The full script is disclosed on request in a read-only viewer (never published on GitHub); the attestation binds to this exact hash.