audit_2.md — verification ledger
audit_2.md is a dump of two different models' output concatenated into one file, so it
contains duplicate findings and an internal contradiction. This ledger is an independent
re-check of every finding against the tree at cb7624a, done by reading the real files rather
than the audit's own assertions.
Method
Every finding was re-read from audit_2.md and then checked against the source it names. The
audit's file paths are frequently abbreviated or simply wrong, so the real file was located
first and quoted. Verdict vocabulary:
| Verdict | Meaning |
|---|---|
| real | The claim matches the current source as stated. |
| partial | The code is as described, but a stated consequence, magnitude, location or severity is wrong. |
| refuted | The code does not say what the audit claims. |
| stale | Already fixed / no longer present in the tree. |
| unverifiable | Needs a device, a live database or a benchmark to settle. |
Deployment context (checked first, because it changes two verdicts)
This repository is hosted on OCI: PostgreSQL 16 + PostgREST + Caddy, with Supabase used
only for Auth. There is no Supabase-hosted database. apps/web/src/backend/config.ts:2
records the two data providers ("supabase" | "postgres"), :22 says dataUrl is "Where the
data API lives, when it is not Supabase's", and :14-18 says the self-hosted PostgREST speaks
the same protocol as Supabase's REST endpoint and validates the same JWT secret.
Two consequences:
- Everything in
supabase/migrations/*.sqlruns against the OCI Postgres. Thecompact_crdt_log,metric_historyandlatest_metric_activityfunctions the audit discusses are all live on the OCI box, not on a Supabase database. - The file names say
supabase-*. That is the historical provider id, not the host. The adapter module is named after the protocol it speaks (PostgREST), not the vendor.
Two findings that are already implemented as the audit prescribes
These two are the top of the audit by score, and both are wrong in the same direction: the audit describes a fix that is already in the tree and reports it as missing.
WF-N01 — refuted (was scored 8.6, "Critical")
Migration supabase/migrations/0132_compact_crdt_log_rpc.sql already does exactly what the
finding's fix block asks for, in one function body:
update %I set snapshot_upto = greatest(coalesce(snapshot_upto, 0), $1)
where id = $2 returning snapshot_upto -- :96-97 forwards-only, in-statement
...
delete from public.crdt_updates
where resource_type = p_resource_type
and resource_id = p_resource_id
and id <= v_upto -- :112 the watermark just written
- The forwards-only guard is inside the statement (
greatest), so a stale caller cannot rewind the watermark. - The sweep is bounded by
v_upto— the watermark the function just read back — not byp_upto_id, which is precisely the "never by the client'suptoId" requirement. - The caller-side comparison at
compact-crdt-log.use-case.ts:43is documented as a courtesy early-out, not the guard:crdt-update-store.ts:66-69says "Lets a stale caller be a local no-op rather than a round trip; the database decides anyway."
The finding's own SQL block is structurally the same as the migration. There is no delete-before-watermark race and no data loss. Nothing to change.
WF-N04 — partial (was scored 7.0, "High")
supabase-metric-repository.ts:9 does use one constant TABLE = "experiment_metrics" for both
the read and the append insert, and :48 does this.db.from(TABLE).insert(payload). The
description of the code is accurate. The stated consequence is not:
Migrations 0114:321-322 and 0115:136-139 install INSTEAD OF INSERT/UPDATE/DELETE
triggers on the view:
create trigger experiment_metrics_insert_trg
instead of insert on experiment_metrics
0114:305-307 shows the trigger routing into experiment_metric_points and coalescing the
owner — coalesce(new.user_id, auth.uid()), commented as "the table default the view cannot
carry". So append cannot fail with "cannot insert into a view": it has a working, documented
write path.
The finding's prescribed fix — insert straight into experiment_metric_points — would
fail, because the payload the port takes carries the metric text, while the row table
requires metric_id, and user_id has no column default (0114:161). The view exists to do
that name lookup.
The residual true half: append has no production caller (the Python SDK is the writer over
the ingest API), so it is unexercised against the real database. That is a test gap, not a
broken write path — and it is the same code state that MEM-04 describes from the other side.
The real, actionable findings
Grouped by area; each keeps the audit's id so the two documents cross-reference. "Effort" is a re-estimate after reading the code, not the audit's.
Data layer
| id | verdict | What was wrong with the audit's version | Effort |
|---|---|---|---|
| WF-N11 / WF-N16 (dup) | real | None. Two findings, one root cause: the budget is optional in the port signature. Fixes are identical, so they land as one change. | S |
| WF-N12 | real | None, except that the recorded malformed array literal was the local PGlite client (pglite-client.ts:135) and that defect is already fixed. Root cause is distinct from N11/N16: unbounded writes, not unbounded reads. |
XS |
| WF-N04 / MEM-04 (dup) | partial | See above. The real work is a round-trip integration test, not a repoint. | S |
| MEM-04 | partial | "nothing implements or calls it" is false: two adapters implement append and the shared contract suite exercises it. Also append sits on a separate IMetricWriter port (metric-point.ts:27-29), so deleting it from IMetricRepository would delete nothing — it is already segregated. |
— |
Lexical extractor (packages/core)
| id | verdict | Correction | Effort |
|---|---|---|---|
| WF-N06 | real | The worked example is off: in "MAGAN GAN Models", phrase.indexOf("GAN") is 2, not 5. Defect and direction unchanged. |
XS |
| WF-N07 | real | None. | XS |
| WF-N13 | real | None — the shared CANDIDATE.lastIndex is a genuine latent hazard, not an active bug (no await sits between the reset and the scan today). |
XS |
| WF-N15 / WF-N20 (dup) | real | None; N20 is the residual slice of N15 after N15's lifetime fix. | XS |
| WF-N17 | partial | The magnitude is wrong. indexOf returns at the first hit, so a hashtag present in plain is not "a full scan of the document"; "80 tags costs 160 document scans" is overstated. Only wikilink targets — blanked out of plain by stripMarkdown — cost two near-full scans. It is still worth deleting evidenceFor by threading the index through. |
S |
| WF-N18 | real | Understated, if anything: extractWikilinks → vault-page.ts:148-152 maskCode adds two more full-body replaces. |
M |
Android shell (apps/android)
| id | verdict | Correction | Effort |
|---|---|---|---|
| WF-N02 | real | None. Highest-value Android fix in the document: left-handed writers put the palm down first nearly every stroke. | M |
| WF-N03 | real | None. The finding's own analysis is correct, including why option (b) is unavailable while the overlay is a SurfaceView sibling. |
S |
| WF-N08 / WF-X08 (overlap) | real / partial | X08's "discovered" claim is refuted by the block it quotes: javaScriptEnabled and domStorageEnabled are about what the WebView may reach. Also setAllowFileAccessFromFileURLs is a deprecated no-op at minSdk 29. |
S |
| WF-N09 | real | None. Network security config is strictly less code than the global flag. | S |
| WF-N10 | real | None. | S |
| WF-N14 | partial | Two sub-claims are false: the list is ArrayList<Float>, not nullable (:67), and the buffer is cleared per stroke (:208-209, :141). The real defect is the unbounded per-stroke buffer plus JSONArray(strokePoints).toString() on the UI thread at pen lift. |
S |
| WF-N19 | wrong target | The code reading is accurate — apps/android really does have isMinifyEnabled = false and no signingConfig — but that module is not the shipped Android app. See below. |
— |
WF-N19 — accurate about the code, wrong about which artifact ships
Found on a second pass, after being asked why a release-signing finding existed at all. There are two Android projects and the audit's framing merged them:
apps/web/twa |
apps/android |
|
|---|---|---|
| What it is | The Bubblewrap Trusted Web Activity | The inking shell — a WebView plus the stylus surface |
| Package | app.weaveforge.twa |
org.weaveforge.ink |
| Shipped? | Yes — Play releases on android-v* tags, and the CI workflow builds, checks and signs it |
No. Debug only, side-loaded by the owner; apps/android/README.md lists "Release signing" under Not done |
| Signing | Configured: keystore from CI secrets, app-release-signed.apk + .aab committed to the workflow's artifacts, keystore present locally |
None |
| Minification | minifyEnabled true — apps/web/twa/app/build.gradle:162-163 |
isMinifyEnabled = false |
| Size on disk | 1.09 MB signed release | 8.6 MB debug |
So the finding's two recommendations — add a signingConfig, turn on R8 — are
already done on the artifact that ships, and the only place they are absent is
a developer's own debug shell, for which an unsigned, unminified build is the
correct configuration. apps/android/README.md says as much in its first
paragraph: "This is not the TWA in apps/web/twa — a Trusted Web Activity
runs in Chrome Custom Tabs, which allow no native view on top."
The audit even quotes that README's "Not done: Release signing" line as evidence of a gap, when it is a statement of scope.
Adding R8 to the inking shell would also have been the one change here that can only
fail on a device: R8 strips the reflection-called @JavascriptInterface methods
unless a keep rule holds them, and inking then stops working silently in release
only — untestable on this machine. That is a bad trade for an artifact nobody
installs.
D3 is dropped, not deferred. The action it leaves is documentation: the audit's finding should be read as "the inking shell is a debug tool", which its README already says.
Permission posture (WF-X01…WF-X08)
| id | verdict | Note |
|---|---|---|
| WF-X01 | real | The posture is accurate and the recommendation is the single best-value item in the audit. Two secondary errors: check-dry.mjs's select("*") rule is a plain ban with no allow-list (the allow-list pattern the fix cites belongs to a different rule, check-dry.mjs:35-64), and the code block's FORBIDDEN_PERMS omits MEDIA_PROJECTION while the prose requires it. |
| WF-X02 | real | Forward-looking constraint; no overlay exists today. |
| WF-X03, WF-X04, WF-X05 | real | All three are contracts for capabilities that do not exist. Their file paths are explicitly marked "(proposed)". |
| WF-X06 | partial | Presented as "Verified in source" but its second half describes a service that does not exist, and its first premise ("Read the brief's framing") cites a brief that is not in this tree. Should be labelled proposed. |
| WF-X07 | real | The diagnostic screen does not exist and needs no new permission. |
| WF-X08 | partial | See the Android table. |
Audit-internal contradictions (the "two models" problem)
These are the places where the file argues with itself. None needs a user decision; they need the wrong half deleted.
ACC-01vsWF-X01— the sharpest one.ACC-01(audit_2.md:1536-1547) claims "When users grant accessibility or device admin permissions to the app, they cannot use payment apps anymore", marks itselfstatus: "confirmed", and prescribes keeping the capability and warning the user, plus "event filtering to ignore TYPE_WINDOW_STATE_CHANGE for payment apps".WF-X01(:504-510) states the opposite and is right: the repo contains exactly two Kotlin classes, neither aService, and "There is no code path in this repository that could disable, delay or obstruct a payment app".WF-X03further shows that event-type filtering does not help and must not be the mechanism.Verified against the tree:
AndroidManifest.xml:4declaresINTERNETand nothing else, there is no<service>element at all, and a repo-wide search for accessibility-service symbols matches onlyaudit_2.md.ACC-01is refuted;WF-X01/WF-X03stand.ARCH-02andARCH-04are each stated twice, once as broken and once as fixed (:1026vs:1738;:1418vs:1765). The current source agrees with the later entries. The earlier ones are pre-fix snapshots left in place.MEM-04contradictsWF-N04on whetherappendexists, and contradicts its own fix text. Both are wrong in different ways; see the table above.ARCH-08andARCH-09both claimproposalAppliesas their fix. It isARCH-09's — the append-note revision predicate.ARCH-08's actual deliverable isZoteroPushOutcome. The file assigns the same symbol to two findings (:1820vs:1833).PERF-03's mechanism and location are wrong even though the fix landed. The old cost was a linearfindper lookup —O(N×M)per column, perpackages/core/src/features/papers/application/compute-rollup.ts:28-29— not "every row compared against every other row", and the code lives infeatures/reading-lists/application/extraction-table.ts, notfeatures/experiments.PERF-04's "27 sites remain as a counted ratchet" is wrong.check-dry.mjs:202-208says it found 45 sites, all 45 are now named, and "this is a plain ban with no baseline to keep in step". The gate reports 0 today.PERF-13's counts are wrong (86/34 files, max 10, vs the file's own 95/40, max 14) and 0 remain today, not 95.ACC-05overstates the keyboard gap: modals do trap and restore focus correctly (components/modal.tsx:63-92). The residue is drag-and-drop and arrow-key menus.ACC-06's consequence is wrong: no image in the app lacks analtattribute. The defect is non-descriptive alt text ("Paper figure" for every figure) plus no data-table alternative for charts.
Duplicate clusters (one fix each, not two)
| Cluster | Relates to | Distinct deliverable |
|---|---|---|
| WF-N11 + WF-N16 | the same optional maxPoints budget |
make the budget required |
| WF-N04 + MEM-04 | the same append |
one round-trip integration test |
| WF-N15 + WF-N20 | eager strings in prepare() |
prepare/harvest/drop, then lazy rawLower |
| MEM-05 + ARCH-16 | describeRejection prose moved out of core |
already done — both are stale |
| MEM-06 + ARCH-15 | the two freshness constants | already done — both are stale |
| ARCH-08 + ARCH-09 | both cite proposalApplies |
ARCH-09 owns it |
Findings already fixed in the tree (stale, delete from any plan)
WF-N01, BUG-02, BUG-16, PERF-01, PERF-02, PERF-04 (the gate), PERF-14,
SEC-04, MEM-01, MEM-02, MEM-03, MEM-06, ARCH-06, ARCH-14, ARCH-15, ARCH-16.
BUG-19 is refuted: postgrest-transport.ts:101 persists
serverVersion: typeof version === 'number' ? version : null, and :86-90 says so explicitly
("serverVersion: null — not 0 … a fabricated 0 becomes a guard that matches nothing"). The
finding's fix text has both halves backwards.
ARCH-07 is refuted: this.scoped(...) exists and is used at eight call sites
(project-scoped-repository.ts:34). Only the exact identifier PAPER_ID_COLUMNS is absent —
it is spelled PAPER_IDENTITY_COLUMNS.
Unverifiable without a device or a benchmark
LEDGER-Phase 3's "5624 ms → 5 ms" rollup measurement. The code and the 1500 ms budget guard exist; the two numbers are prose. Re-run the extraction-table benchmark at a 400-row / 20 000-value table to settle it.PERF-05's "doubling latency" between the scope read and the token mint. The two RPCs are real; the magnitude is not measured anywhere in-tree.- Every simulator-only Android claim (
WF-N02,WF-N03,WF-N14): the code reading is conclusive, but the user-visible behaviour needs a stylus and a palm.