Skip to content
Back to skills

Extract Scan

ASecurity

Exports and dumps as faithful copies; OFFSET paging of mutating tables, no snapshot, silent caps, lossy values.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 20, 2026
ai-agentspythongojavarubyphpbashsqlnodedjangogit

Works with

  • cursor
  • api

Security analysis

A100/100

Scanned October 20, 2026

npx -y skills add adnanmokhtar/refract --skill extract-scan --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Extract Scan?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Extract Scan
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/adnanmokhtar-extract-scan/badge)](https://www.skillsdirectory.com/skills/adnanmokhtar-extract-scan)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: extract-scan
description: "Exports and dumps as faithful copies; OFFSET paging of mutating tables, no snapshot, silent caps, lossy values."
kind: skill
pack: data-engineering
allowed-tools: [Read, Grep, Glob, Bash]
---

# Skill: extract-scan

## Premise

An export is a copy someone acts on: a merchant reconciles a CSV against their bank, a partner loads a nightly file, an admin restores a dump. It is a faithful copy only if every row of the source **at one instant** appears **once**, with its values intact. Four mechanisms break that without an error: paging a table that is being written by `OFFSET` (newest-first, a row inserted at the head shifts every later page by one and a boundary row appears twice; a delete behind the cursor skips one), reading several tables in separate statements with no shared snapshot (a child row whose parent was read before it existed), a cap that truncates silently or a file published before the run finished, and a representation that loses data on the way out (an instant with no offset, a decimal through a float or a locale format, an id or a phone number through `Number`). Use The Index, Luke's "No Offset" and Laravel's `chunk` vs `chunkById` warning (research B26, B27) describe the first; the Postgres manual's transaction isolation chapter the second.

This skill owns research ids EX-1…EX-4 (`research-data-engineering.md` § 1.7), shipped as `DE-EX-<n>`: **whether an extract, export or dump is a faithful, consistent copy of its source.**

**Every finding cites the extract `<file:line>` + the source table and its write rate (does anything write it during the export?) + the paging key and the transaction boundary as read + the row that is duplicated, skipped, orphaned, truncated or altered + the oracle that would settle it + the fix in the project's own primitive.** A grep hit is a candidate. "The exports look fine" is never printed; the output is the class-coverage block (§ Output). Verification follows [`refute-before-report.md`](../../../../snippets/refute-before-report.md) with [`precedents/data-engineering.md`](../../../../snippets/precedents/data-engineering.md); the oracles are [`proof-oracles.md`](../../../../snippets/proof-oracles.md) § Data engineering, V560–V562.

## The rule

A candidate is a finding when all four hold:

1. **Reachable and consumed.** A route, job, queue processor or script produces the file or the push, and a person, a partner or a restore reads it. A one-off script that already ran fails here unless it is the documented path.
2. **The source is mutable during the read.** The table takes inserts, updates or deletes while the export runs (an order table, an activity log, a ledger with reversals). An immutable partition, a closed period, or a source stopped for the dump is precedent § 30 / the quiescent-source note.
3. **No existing control, across files.** Keyset paging on a unique key compared in SQL, one `REPEATABLE READ` / `SERIALIZABLE READ ONLY DEFERRABLE` transaction or an exported snapshot (`pg_export_snapshot()`), `pg_dump` (one snapshot by design), a ceiling that refuses with a message (precedent § 39), a temp-then-rename publish, ISO-8601 instants with an offset, decimals as strings, ids as text cells.
4. **The oracle fired, or the fact is two-ended.** V560/V561/V562 ran red against a scratch DB with a concurrent writer; or the static fact cites both ends — the advancing `page` and the newest-first order on a table the same module inserts into; the per-table statements and the absence of any isolation setting.

## Boundaries

- **The per-query cost of a deep OFFSET and a non-unique sort key on a paged API read** → database `query-shape-scan` §2/§8 (when installed); this skill owns the **consistency** of a multi-page copy, and a tie-breaker missing from an export's sort is §1 here with §8 corroborating. **Whole-file buffering and export memory** → performance `request-path-scan` §6 / `memory-growth-scan`. **CSV formula injection** → security `injection-scan`. **Who may export, and PII in the export** → security `@data-privacy-reviewer` and `pii-copy-scan` §5 (this pack). **API wire formats** → backend `wire-format-scan`. **A half-written temp file left on disk** → code-quality `error-handling-scan` §7. Each when installed; without them the row is `[report-flagged]` here with the route named.
- **Warehouse loads** that read an OLTP table incrementally → `incremental-load-scan`; **a migration that drops a column an extract reads** → `contract-diff` §4.

## Adapt to the codebase — the lanes

| Lane | Detect by | Paging / snapshot idiom read | Fix primitive |
|---|---|---|---|
| **Node / TS** (MikroORM, Prisma, TypeORM, Drizzle, Knex, Sequelize; exceljs, csv-stringify, fast-csv, papaparse, xlsx) | export/sheet/dump/runner/sync files; writer imports | `page` / `offset` / `skip` advancing; `cursor`, `lastId`, `id < $last`; `em.transactional(…, { isolationLevel })`, `$transaction(…, { isolationLevel })` | keyset on `(created_at, id)` compared in SQL; one `REPEATABLE READ` read-only transaction; stream from a server-side cursor |
| **Python** (Django, SQLAlchemy, pandas; `csv`, openpyxl, xlsxwriter) | `csv.writer`, `to_csv(`, `.iterator()`, `yield_per` | `[offset:offset+n]` slicing, `.iterator(chunk_size=)`, `Paginator` | `.iterator()` inside `transaction.atomic()` with `REPEATABLE READ`; keyset filter on `pk` |
| **Ruby / PHP** (ActiveRecord, Eloquent; `CSV.generate`, PhpSpreadsheet) | `find_each`, `in_batches`, `chunk(`, `chunkById(`, `cursor()` | `chunk` pages by OFFSET; `chunkById`/`find_each` page by key | `chunkById` / `find_each` on the PK |
| **Dumps** (`pg_dump`, `mysqldump`, `COPY … TO`, app-level table loops) | dump scripts, CI jobs, `information_schema.tables` loops | per-table statements; `--single-transaction`; `pg_export_snapshot` | `pg_dump` (one snapshot); `mysqldump --single-transaction` for InnoDB; one `REPEATABLE READ` transaction around the loop |
| **Pushes** (Google Sheets, partner SFTP, S3/GCS drops, webhooks) | `googleapis`, `sftp`, `putObject`, `upload(` in export files | publish order, temp names, completion markers | write to a temp key, rename on success; a manifest written last |

## Pre-flight

Run as **one** script from the repo root (bash 3.2-safe). Grep **N** feeds §N; 0 builds the surface. Every `rg` names its path; include globs first, exclusions last, every exclusion `**/`-rooted; **the export path glob stands alone** — it is never combined with an extension glob (ripgrep ORs two positive `-g` globs: path glob + code glob listed 31 files on a real repo where the path glob alone listed the runners). Every grep ends in `ck`: rg exit 2 is a **blocked** class, never zero hits. Nothing is written outside `$OUT`.

```bash
ROOTS=(.)   # monorepo: name the server roots, e.g. ROOTS=(apps/api/src)
OUT="${OUT:-$(mktemp -d "${TMPDIR:-/tmp}/de-ex.XXXXXX")}"; mkdir -p "$OUT/none"
h() { printf '## %s\n' "$1"; }; ck() { [ "$2" -lt 2 ] || echo "$1 BLOCKED (rg exit $2) — the class is blocked, never 0 hits"; }
X=(-g '!**/node_modules/**' -g '!**/vendor/**' -g '!**/{dist,build,out,target,coverage,.next,.venv,venv,logs}/**'
   -g '!**/{test,tests,__tests__,spec,specs,e2e,fixtures,__fixtures__,mocks,__mocks__,testdata}/**'
   -g '!**/*.{spec,test,e2e-spec,stories,d}.*' -g '!**/test_*.py' -g '!**/*_test.{py,go}'
   -g '!**/{locales,locale,i18n,translations,migrations,migration}/**' -g '!**/{docs,ai,.claude}/**' -g '!**/*.{md,mdx,lock,min.js,snap,csv,json}')
CODE=(-g '*.{ts,mts,js,mjs,cjs,py,rb,php,java,kt,go,cs,sh}')
NC=(rg -v --pcre2 '^[^:]+:\d+:\s*(?://|/\*|\*(?:\s|/|$)|#|--)')
# 0 THE EXTRACT SURFACE — 0a export/sheet/dump/runner/sync files by PATH (glob alone) · 0b files that write a file format or dump · union = EXP
h 0a; rg --files -g '**/*{export,Export,sheet,Sheet,dump,Dump,extract,Extract,runner,Runner,sync,Sync,backup,Backup}*' "${X[@]}" "${ROOTS[@]}" | rg -v --pcre2 '\.(?:md|json|ya?ml|csv|txt|svg|png)$' | sort -u > "$OUT/EXP0.txt"; wc -l < "$OUT/EXP0.txt"
h 0b; rg -l --pcre2 '(?:from|require\()\s*["\x27](?:exceljs|csv-stringify[\w/]*|fast-csv|@fast-csv/\w+|papaparse|json2csv|xlsx|write-excel-file|archiver)["\x27]|\bcsv\.(?:writer|DictWriter)\(|\.to_csv\(|\bopenpyxl\b|\bxlsxwriter\b|PhpSpreadsheet|\bCSV\.(?:generate|open)\b|\bcopy_expert\(|\bcopyTo\(|(?i:\bcopy\s+\(?[^;\n]*\bto\s+(?:stdout|program)\b)|\bpg_dump\b|\bmysqldump\b' "${CODE[@]}" "${X[@]}" "${ROOTS[@]}" | sort -u > "$OUT/EXP1.txt"; ck 0b ${PIPESTATUS[0]}; wc -l < "$OUT/EXP1.txt"
cat "$OUT/EXP0.txt" "$OUT/EXP1.txt" | sort -u > "$OUT/EXP.txt"; E=(); while IFS= read -r f; do E+=("$f"); done < "$OUT/EXP.txt"; [ ${#E[@]} -gt 0 ] || E=("$OUT/none")
# 1 EX-1 multi-page reads — 1a a page/offset that advances into a list/find query · 1b the keyset controls in the same files (read each pair)
h 1a; rg -n --pcre2 '(?:async\s*)?\(\s*page\s*(?::\s*number)?\s*\)\s*=>|^\s*page\s*,\s*$|\bpage\s*:\s*page\b|\bpage\s*(?:\+\+|\+=\s*1)|\bpage\s*=\s*page\s*\+\s*1|\boffset\s*(?:\+=|=\s*offset\s*\+)|\.skip\(\s*[a-z]\w*|\boffset\s*:\s*\(?\s*(?:page|i|n)\b|(?:->|\.)chunk\(\s*\d+|(?i:\boffset\s+(?:\$\d+|\?|%s|:\w+|\$\{))|\.offset\(\s*[a-z]\w*|\[\s*\w+\s*:\s*\w+\s*\+\s*\w+\s*\]' "${E[@]}" | "${NC[@]}" | tee "$OUT/P1.txt"; ck 1a ${PIPESTATUS[0]}
P=(); while IFS= read -r f; do P+=("$f"); done < <(cut -d: -f1 "$OUT/P1.txt" | sort -u); [ ${#P[@]} -gt 0 ] || P=("$OUT/none")
h 1b; rg -c --pcre2 '\bpage\s*:\s*1\b|\blastId\b|\blast_id\b|\bcursor\b|\bafter\s*[:=(]|\bbelow\(|\bchunkById\(|\bfind_each\b|\bfind_in_batches\b|\bin_batches\b|\.iterator\(|\byield_per\(|\bkeyset\b|(?i:"?id"?\s*[<>]\s*(?:\$\d+|\?|:\w+|%s))' "${P[@]}"; ck 1b $?
h 1c; rg -n --pcre2 '(?i)\b(?:defaultSort|orderBy|order_by|order)\s*[:=(]\s*[\[{(]?\s*["\x27]?\w*(?:created|updated)_?at["\x27]?\s*(?::\s*["\x27]?desc|,\s*["\x27]?desc|\s+desc)|\bfield\s*:\s*["\x27]\w*(?:created|updated)_?at["\x27]\s*,\s*direction\s*:\s*["\x27]desc|\border\s+by\s+"?\w*(?:created|updated)_?at"?\s+desc\b(?!\s*,)|->latest\(\)|\.order\(\s*(?:created_at|updated_at)\s*:\s*:desc' "${CODE[@]}" "${X[@]}" $(for f in "${P[@]}"; do [ -f "$f" ] && dirname "$(dirname "$f")"; done | sort -u) "$OUT/none" | "${NC[@]}"; ck 1c ${PIPESTATUS[0]}
# 2 EX-2 multi-table reads with no shared snapshot — 2a table loops/catalog reads with no isolation in the file · 2b mysqldump without --single-transaction
h 2a; rg -l --pcre2 '(?i)\binformation_schema\.tables\b|\bpg_tables\b|\bpg_class\b|\bshow\s+tables\b|\bfor\s*\(\s*const\s+\w+\s+of\s+\w*tables\b|\bfor\s+\w+\s+in\s+\w*tables\b|\btables\.(?:map|forEach)\(|\bget_table_names\(' "${E[@]}" | tr '\n' '\0' | xargs -0 -r rg --files-without-match --pcre2 '(?i)repeatable[\s_]+read|\bserializable\b|pg_export_snapshot|set\s+transaction\s+snapshot|--single-transaction|isolation_?level|IsolationLevel\.|REPEATABLE_READ|\bpg_dump\b'
h 2b; rg -n --pcre2 '\bmysqldump\b(?![^\n]*--single-transaction)' "${CODE[@]}" -g '**/Makefile' -g '**/*.{yml,yaml}' "${X[@]}" "${ROOTS[@]}" | "${NC[@]}"; ck 2b ${PIPESTATUS[0]}
# 3 EX-3 caps and publication — 3a caps in extract files (read whether they refuse) · 3b the refusal and failure vocabulary per file · 3c where the stored path / ready status is written
h 3a; rg -n --pcre2 '\bslice\(\s*0\s*,\s*(?:[A-Z_][A-Z0-9_]*|\d{3,})\s*\)|\.limit\(\s*(?:MAX\w*|\d{3,})\s*\)|(?i:\blimit\s+\d{3,}\b)|\btake\s*:\s*(?:MAX\w*|\d{3,})|\[\s*:\s*(?:MAX\w*|\d{3,})\s*\]|\.head\(\s*\d{3,}\s*\)|\bMAX_(?:ROWS|EXPORT\w*|LINES)\b' "${E[@]}" | "${NC[@]}" | tee "$OUT/P3.txt"; ck 3a ${PIPESTATUS[0]}
Q=(); while IFS= read -r f; do Q+=("$f"); done < <(cut -d: -f1 "$OUT/P3.txt" | sort -u); [ ${#Q[@]} -gt 0 ] || Q=("$OUT/none")
h 3b; rg -c --pcre2 '(?i)\bthrow\b|\braise\b|\brefus\w*|toolarge|too_large|\bexceed\w*|\b413\b|\btruncat\w*' "${Q[@]}"; ck 3b $?
h 3c; rg -n --pcre2 '(?i)\bstatus\s*[:=]\s*["\x27](?:ready|done|completed?|available|succeeded)["\x27]|\b(?:file_?url|download_?url|storage_?path|s3_?key|object_?key|file_?path|file_?key)\s*[:=]|\b(?:putObject|upload|uploadFile|writeFile|rename)\s*\(' "${E[@]}" | "${NC[@]}"; ck 3c ${PIPESTATUS[0]}
# 4 EX-4 representation — locale/float/Number on amounts and ids, zone-less instant formatting, in row mappers
h 4; rg -n --pcre2 '\.toLocale(?:String|DateString|TimeString)\(|\.toFixed\(|\bparseFloat\(|\bNumber\(\s*[\w.?!]*\b(?:amount|price|total|balance|cost|fee|subtotal|cents|id|phone|mobile|msisdn|iban|code)\w*\s*\)|new\s+Date\([^)]*\)\.toString\(\)|\.get(?:Hours|Date|Month|FullYear)\(\)|\bstrftime\(\s*["\x27](?![^"\x27]*%z)[^"\x27]*%H|\bformat\(\s*[\w.]+\s*,\s*["\x27][^"\x27]*HH(?![^"\x27]*(?:X|Z|xxx|O))[^"\x27]*["\x27]\s*\)|\bfloat\(\s*\w+\.(?:amount|price|total)' "${E[@]}" | "${NC[@]}"; ck 4 ${PIPESTATUS[0]}
```

1b prints, per extract file, how many keyset or snapshot markers it holds: a file with 1a lines and a 1b count of 0 is the strongest §1 candidate; a file with both (a real repo's orders export: `page: 1` + `below(lastId)` beside an activity loop that advances `page`) is read method by method — one method can be the control and the next the defect. 1c lists newest-first orders in the modules that hold a 1a file (the runner's list call usually sorts in a port beside it): an advancing page over a newest-first order on an insert-heavy table duplicates the boundary row.

### Measured (2026-10-19)

- **Fixture** (`benchmarks/fixtures/data-pipelines`, `ROOTS=(.)`; under 1 s, 0 `rg` errors): 0a 19 path-matched files · 0b 5 writer files. 1a 5 lines (the activity runner's `page` callback, the customers runner's `offset +=`, and the shared `collectPages` helper) · 1c 2 newest-first orders in the same modules · 2a 1 (the platform dump; the `REPEATABLE READ` tenant export is excluded) · 2b 1 (`mysqldump` without `--single-transaction`) · 3a 3 (the silent `slice(0, MAX_EXPORT_ROWS)`; the dump's `limit 1000` page size, refuted) · 3b 0 refusals in those files · 3c 2 (`status: 'ready'` before the upload) · 4 2 (`toLocaleString`, `Number(phone)`). The keyset orders export and the refusing ceiling are not listed — they are the controls an agent should not report.
- **Real repo, read-only** (the NestJS monorepo, `ROOTS=(apps/api/src)`; 0.2 s; nothing written): 0a 114 · 0b 0 (its sheets are written by an in-house writer) · 1a 14 lines in six runners — the activity and customers runners page newest-first insert-heavy tables by OFFSET (the eval's two live EX-1 candidates), the wallet and system-wallet runners page ledgers (order and mutation rate decide), and `collectPages` itself · 1b: only the orders export holds keyset markers (`page: 1` + `below(lastId)`, compared in SQL — precedent § 30) · 1c 29 newest-first sorts in those modules · 2a 2 (the dump port: catalogue loop, per-table statements, no isolation — the eval's EX-2 candidate) · 3a 7 (two DSAR export caps that print a "truncated" marker, error-text truncation) · 4 10 (`(cents / 100).toFixed(2)` display formatting of integer minor units — read; not lossy below 2^53). **A first draft** combined the path glob with a code-extension glob and listed 31 files; a second listed newest-first sorts across the whole tree (70+ lines) — now only the modules that hold a paging file.
- **The Python, Ruby, PHP and dump-script lanes are fixture-measured only.**

## The 3,000-hit path

1. **Inventory the extracts once** (0a/0b) and group by **base helper** — a shared `collectPages` or export base class is one decision for every runner that calls it; fix it there.
2. **For each extract, read three facts**: the source table's writers (does the module insert into it?), the order (newest-first?), the transaction boundary (one snapshot or one per page). A row with all three facts is decided; the rest are `[report-flagged]`.
3. **Rank** by consumer: partner and financial files first (reconciled against money), then customer-facing exports, then admin dumps.
4. **Prefer the concurrent-writer oracle at scale**: one harness that runs every runner against a scratch DB while a writer inserts at the head and deletes behind the cursor, comparing exported id sets with a start snapshot (V560).
5. **Ledger the rest** as `Live, unreviewed (k extracts / n hits)` with the command that continues them. Never `checked`.

## Scans for

Severity assumes the file is acted on (reconciled, loaded, restored). One step lower for an ad-hoc admin download.

| § | Class (`DE-` id) | Default severity | Oracle |
|---|---|---|---|
| 1 | EX-1 multi-page extract over a mutating table by OFFSET or a non-unique sort | medium (high for financial or partner files) | V560 |
| 2 | EX-2 multi-table extract or dump not read from one snapshot | medium | V561 |
| 3 | EX-3 silent truncation at a cap, or a partial file published on failure | medium | V562 |
| 4 | EX-4 representation loses data (zone, decimals, ids) | medium | V562 |

### 1. OFFSET paging over a mutating table

```
BAD:   collectPages(async (page) => this.activity.list({ ...filter, page, pageSize: 500 }))   // createdAt desc; the log is appended to
       Order::orderBy('created_at', 'desc')->chunk(1000, fn ($rows) => …)                      // Laravel chunk pages by OFFSET
GOOD:  where (created_at, id) < ($lastCreatedAt, $lastId) order by created_at desc, id desc limit 500   -- compared in SQL
       ->chunkById(1000, …) · find_each · .iterator() inside one REPEATABLE READ transaction
```
**Read the code:** Does anything write the source table while the export runs (the same module's inserts, a webhook, a job)? In which order does the list query sort — newest-first means every insert lands in front of the cursor and shifts the pages; oldest-first with an append-only table is safe for inserts but not for deletes? Is the sort unique (a `created_at` tie between two rows makes page boundaries non-deterministic)? Is the loop inside one snapshot (precedent § 30)? Is a keyset value compared in SQL or copied through a JS `Date` that drops microseconds (a real repo's comment names exactly that risk)? **Finding when** an insert or delete during the export duplicates or skips a row in the file. **Proof:** V560 — insert at the head and delete behind the cursor between pages on a scratch DB: the exported id set equals the start snapshot, with no duplicate.

### 2. Multi-table read with no shared snapshot

```
BAD:   for (const t of tables) { for (let after = null; ; ) { rows = await db.execute(`select * from ${t} where id > ? …`) } }
GOOD:  await em.transactional(dump, { isolationLevel: IsolationLevel.REPEATABLE_READ, readOnly: true })
       pg_dump (one snapshot) · mysqldump --single-transaction (InnoDB) · pg_export_snapshot() shared by parallel readers
```
**Read the code:** Are the tables (or the pages of one table) read in separate statements on separate connections, outside a transaction? Do foreign keys connect them (child after parent)? Is the source quiescent during the dump (maintenance mode, a replica stopped at an LSN)? `pg_dump` is consistent by itself; `mysqldump` needs `--single-transaction` for InnoDB. **Finding when** a write during the dump produces a file that cannot be restored with FKs on, or that shows a state the database never had. **Proof:** V561 — concurrent parent/child inserts during the dump: restore into a scratch DB with FKs on succeeds.

### 3. Silent truncation, or a partial file published

```
BAD:   rows.slice(0, MAX_ROWS)                     // the merchant gets 50,000 of 61,214 rows and no message
       await jobs.update(id, { status: 'ready', fileUrl }); await upload(stream)   // ready before the upload finished
GOOD:  if (count > MAX_ROWS) throw new ExportTooLarge(count, MAX_ROWS)   // precedent § 39
       upload to <key>.partial, rename on success, mark ready last; on failure delete the partial and mark failed
```
**Read the code:** What happens at the cap — refuse with a message, split into files, or cut silently? Does the file or the UI say it was capped? In what order are the upload, the stored path and the `ready` status written; what does the failure path do to a half-written object? **Finding when** a consumer can receive fewer rows than the source holds without being told, or a file that a failed run left behind. **Proof:** V562 — a run above the cap refuses or labels; a failure injected mid-upload publishes nothing.

### 4. The representation loses data

```
BAD:   created_at: row.createdAt.toLocaleString()       // server zone, no offset, locale-dependent
       amount: Number(row.amountMinor) / 100            // 0.1 + 0.2 territory; 17-digit totals lose digits
       phone: Number(row.phone)                         // drops the leading zero
GOOD:  created_at: row.createdAt.toISOString() · amount: formatMinor(row.amountMinor) as a string · phone as text
```
**Read the code:** Which zone does each instant come out in, and does the header or the value say so (a `(UTC)` header or ISO-8601 with `Z` is fine)? Are money amounts produced from integer minor units or decimals as strings, or through a float or a locale formatter? Are ids, phone numbers and account numbers written as text cells, or numbers that a spreadsheet will round or strip? **Finding when** the file holds a value that differs from the source value, or an instant a reader cannot place. **Proof:** V562 — round-trip a seeded row (an instant near midnight in a non-UTC zone, `0.1 + 0.2`, a 17-digit id, a leading-zero phone): every value read back equals the source.

## Output

```
extract-scan — lanes: node (mikro-orm 6.4, exceljs 4.4); dumps: app-level table loop; pushes: googleapis
Surface: 0a 14 path-matched · 0b 6 writers · EXP 16

Findings: 2 · Unconfirmed: 2

1. apps/reporting/src/exports/audit-export.runner.ts:40             [report-with-fix]   medium
   §1 EX-1 — `list({ ...filter, page, pageSize })` over activity_log, default order createdAt desc; the module appends
   to activity_log on every request (activity.service.ts:31). V560: 3 inserts mid-export → 3 duplicated rows.
   Fix: keyset on (created_at, id) compared in SQL — the invoices export's `below(lastId)` (invoices-export.runner.ts:88).
2. apps/reporting/src/platform/table-dump.port.ts:48                [report-with-fix]   medium
   §2 EX-2 — per-table pages in separate statements, no isolation anywhere in the file; order_items → orders FK.
   V561: restore with FKs on fails (order_items row with no order). Fix: one REPEATABLE READ read-only transaction.
3. apps/reporting/src/exports/members-export.runner.ts:33           [report-flagged]    medium
   §1 EX-1 — the same OFFSET loop. Unproven link: the members table's write rate during an export. Settling: V560.
4. apps/reporting/src/exports/settlement.mapper.ts:12               [report-flagged]    medium
   §4 EX-4 — paid_at.toLocaleString(). Unproven link: whether the partner parses the column. Settling: V562.

Routed: 1 → database query-shape-scan §2 (deep OFFSET cost on the same loop) · 1 → security injection-scan (formula guard present)
Verification: 11 candidates → 2 findings · 2 unconfirmed · 2 hardening · 5 refuted (self-verified; precedents § 30, § 39)

Class coverage:
  §1 OFFSET paging       checked (1a 9 · 1b 2 files with keyset · 1c 4 → 1 finding, 1 unconfirmed; orders export refuted § 30)
  §2 no snapshot         checked (2a 1 + 2b 0 → 1 finding)
  §3 caps / partial      checked (3a 2 · 3b refusals in both · 3c 3 → 0; ceiling refuses, precedent § 39)
  §4 representation      checked (4 5 → 1 unconfirmed; 4 ISO-8601 with Z → refuted)
```

`[report-with-fix]`: the concurrent-writer or round-trip oracle failed on a scratch DB, or the fact is two-ended (the advancing page + the newest-first order on a table the module writes; the per-table statements + no isolation), and the fix is local to the runner, the base helper or the mapper. `[report-flagged]`: one link is unproven (the write rate, the consumer's parser, the managed restore path); the row names it and the settling oracle. The coverage block lists **all four** §s on every run: `checked (k → m)`, `N/A (<reason>)`, `blocked (<grep>: rg exit 2)`, `→ <owner> (k)` or `Live, unreviewed (k)`.

## False positives / precedents

The verifier reads [`precedents/data-engineering.md`](../../../../snippets/precedents/data-engineering.md) and [`refute-before-report.md`](../../../../snippets/refute-before-report.md) § 4. The ones this skill meets most:
- § 30 OFFSET inside a `REPEATABLE READ` snapshot, over an immutable partition, or bounded by `id <= :max_id` with a unique order; keyset on `(created_at, id)` compared in SQL.
- § 39 a ceiling that refuses with a message; § 34 the controller exporting its own data to its own destination (for the PII question, which is `pii-copy-scan`'s).
- A quiescent source; `pg_dump`; a header that states the zone; ISO-8601 instants with `Z`.

## When to run

- On any diff that touches an export runner, a sheet push, a dump script, a CSV/XLSX mapper or the list query an export calls — `/audit-data-quality`'s extract signal.
- On every whole-repo `/audit-data-quality`, reporting all four §s; before a partner integration reads a new file.

## Halt conditions

- **Do not report a high row as confirmed until a separate verifier has refuted it.** That verifier is not the run that found it, gets the source table, its writers, the paging key, the order and the transaction boundary with their `file:line` — never this run's reasoning — and passes the row through [`refute-before-report.md`](../../../../snippets/refute-before-report.md) §1–§8 with [`precedents/data-engineering.md`](../../../../snippets/precedents/data-engineering.md). Under `/audit-data-quality` that is `@data-quality-auditor` in refute mode, then `@verifier` mode `refute` on every BLOCKER and HIGH; standalone, apply the protocol as a second pass and tag the rows `self-verified`. List the rest as Unconfirmed, Hardening or Refuted; never drop them.
- **Never run an export, a dump or a push against a shared or production database or a real destination.** Oracles run on a throwaway container with the project's migrations and a local writer; a destination is a temp directory or a stub server. A probe that cannot be isolated is `NOT-RUN`.
- **This skill writes nothing into the audited repo** and produces no export file outside `$OUT`. `find <root> -newer "$OUT/EXP.txt"` is empty at the end; a fix is `/data-fix`'s.
- **No `[report-with-fix]` without the V-oracle failing** or a two-ended fact. An advancing `page` alone is a candidate until the source's writers and the order are read.
- **Do not grade a class another skill owns** (§ Boundaries). **Do not print "the exports look fine"** without the coverage block.

## Related

- `/audit-data-quality` dispatches this skill; `@data-quality-auditor` judges EX rows; `/data-fix` closes them with V560–V562 failing first.
- This pack: `pii-copy-scan` §5 (personal data in stored exports), `derived-data-scan` (exports built from rollups), `contract-diff` §4 (a migration under an extract's columns).
- database `query-shape-scan` §2/§8, performance `request-path-scan` §6, security `injection-scan`, backend `wire-format-scan`, when those packs are installed.

## Sources

From the data-engineering research (2026-10-19): Use The Index, Luke — "No Offset" (2014, updated 2023-09-08); Laravel 12 query builder `chunk` vs `chunkById` (MIT docs); PostgreSQL 18 transaction isolation, `pg_export_snapshot` and `pg_dump` docs; MySQL 8.4 `mysqldump --single-transaction`; node-postgres type parsing notes (DE-B-10…13). The real-repo shapes (an OFFSET runner beside a keyset export, a catalogue-loop dump, a refusing ceiling) are from the batch-8 eval § 3 (read-only, 2026-10-19). Counts are from read-only runs of the block above. Paraphrased; no text reproduced beyond identifiers.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.