Skip to content

id-375 ast-dataflow productionisation — RESEARCH {375.1}

RESEARCH — ast-dataflow productionisation {375.1}

Section titled “RESEARCH — ast-dataflow productionisation {375.1}”

Date: 2026-07-27. Method: 5-agent parallel workflow (baseline audit, market scan, 3 fix-design agents), 40 real-CLI invocations measured, live supabase-js 2.105.4 type probes. Full agent reports: ./research/*.md. Consumers: {375.2} spec deltas + implementation waves {375.3}-{375.8}.

Extract-worthy, but not as-is. The market scan found no product in any category that statically joins a Postgres/Supabase schema to type-checker-resolved TS usage sites — the schema-coverage (“built-not-wired”) report is an unoccupied niche, and the entire “audit my vibe-coded Supabase app” market is security-shaped and explicitly does not cover wiring. But the baseline audit proved a naive coverage report built on today’s queries would lie to a non-technical owner: 3 of 4 “unwired” verdicts in the sample were false negatives. The productionisation work is therefore: fix the confidence layer, close the false-negative channels, ship the report, and amortise the cold start.

  • Unique, keep as core: column-reads/writes, type-drift-detect, flow-trace-to-DB-sinks, confidence tiers, the planned schema-coverage report. No competitor in 5 categories searched (DB-side stats tools, raw-SQL checkers like SafeQL/pgTyped, Knip-class, vibe-audit MCPs, ts-morph analyzer CLIs).
  • Commoditised, keep as supporting verbs only: callers/references/ importers (Serena MCP, mcp-ts-morph, scip-typescript, LSPs), dead-exports (Knip — position ours as the Knip verifier).
  • Do NOT build: security/RLS checks, SQL-side lints, autofix.
  • Shape for standalone: one library, two entry points — npx <tool> audit CLI first, thin MCP second. Lead verb = the coverage report; 13 queries as power-user subcommands. Name: wiring/coverage metaphor, not AST (candidates: supawire / wiredup / schemacover); check Supabase brand rules before locking a supa- prefix.
  • Marketing wedge: CADO research (63%+ of retrieved columns unused in DB-backed apps) + “your agent built the table, wrote the UI, forgot the wire”.

3. Baseline audit — why a naive report lies (full: research/baseline.md)

Section titled “3. Baseline audit — why a naive report lies (full: research/baseline.md)”

Corpus: 70 public tables / 807 columns / 62 RPCs / 63 api.* views. 40 measured invocations over 18 columns (mean 9.3 s wall each, 58% = repeated project-load overhead; full-schema serial sweep ≈ 4.2 h).

False-negative channels, each verified with a concrete column:

ChannelExampleRoot cause
.from(TABLE_CONST) variable table argssignup_policy.allowed_domain (a fail-closed auth gate!) reported 0/0findFromCalls matches only string literals; ~22 dynamic sites acknowledged in lib/supabase/schema.ts
SQL function bodiescorpus_writer_fence_lease.holder_token62 RPCs invisible to TS and PY scanners
Python pipelinecontent_propagation_version.payload_checksum (also a const-table case)separate CLI, no result union; sqlglot not installed → regex fallback
RPC param heuristic never fires0 rpc-payload rows in 40 runsrepo prefixes params p_*; reads-side is table-blind, writes-side has no RPC detection at all

False-positive channel: indirect/wildcard tiers are unfalsifiable — a nonexistent column collected 5 indirect writes + 4 wildcard reads; 44 select('*') sites poison every column on those tables. And no existence validation: dropped tables (bid_questions — still the tool’s own CLI example) return clean 0/0 exit 0, indistinguishable from built-not-wired.

True capability confirmed: for literally-addressed hot tables the tool is precise (22 exact reads on form_questions.question_text; feed_articles.extraction_method write-only verdict grep-confirmed true — per-table attribution beat naive grep).

4. detectIsTyped — defect is wider than briefed (full: research/fixCore.md)

Section titled “4. detectIsTyped — defect is wider than briefed (full: research/fixCore.md)”

Probed against real supabase-js 2.105.4: .from('t') on an untyped client returns PostgrestQueryBuilder<any, any, any, "t", unknown> — the table-name literal is echoed into the generic, so branch 1-a (includes(table)) is the dominant false-positive path, not just the briefed structural branch 1-b. Strategy 2 is dead code (getType().getSymbol() resolves to the SupabaseClient interface, never the variable declaration). The fixture stub drops the table-name echo, masking all of this. Real-repo confirmed false exact rows: scripts/eval-classification.ts:228, e2e/global-teardown.ts:65,94.

Validated fix (8/8 probe cases): inspect .from() return-type type arguments for a non-any Relation carrying a concrete Row shape; abandon text matching entirely; fix strategy 2 to use the identifier symbol; apply the same fix to the rpc copy at column-reads.ts:266. Fixture stubs must be upgraded to reproduce the table-name echo (defect-reproduction gate: existing untyped tests MUST fail after stub upgrade, before heuristic fix).

5. Spatial truncation (inv 14) — all 11 result-set queries violate

Section titled “5. Spatial truncation (inv 14) — all 11 result-set queries violate”

Every query caps first-come in ts-morph discovery order. type-evolution additionally stops enumerating at the limit (undercounts totalEstimated). Design: new pure tools/ast-dataflow/truncate.tstruncateSpatial(allRows, limit): stable sort (file, line, column), round-robin by file when over limit, re-sort picked set. Exempt: flow-trace + reexport-chain (rows are path/chain hops, not coverage sets — document exemption). Full integration diff table per query in research/fixCore.md §B.

6. Cache pivot — facts cache rejected by measurement (full: research/fixCache.md)

Section titled “6. Cache pivot — facts cache rejected by measurement (full: research/fixCache.md)”
  • 11 of 12 queries need the live type checker; only string-literal-uses is purely syntactic. No code path consumes “extracted facts” — TECH.md’s §Cache strategy as written = 12-20 h for ~1/12-query benefit.
  • Measured: Project construction ~5 s + first checker build 2.6-3.5 s; warm in-process re-query 80-240 ms; single-file refresh then re-query ~1.6-2.1 s. Held-project RSS ~1.7 GB.
  • id-191’s activation trigger has FIRED on both arms: importers breaches its 5 s P-19 budget in any state (10.5 s cold / 18-20 s warm — independent algorithmic defect: per-import findReferencesAsNodes + corpus×imports resolution loop), and the rename-sweep skill chains 5 cold CLI calls (~60-90 s per sweep).
  • Recommendation (replaces TECH §Cache strategy): warm MCP stdio server (tools/ast-dataflow/mcp-server.ts) holding the Project; single dispatch tool; per-call mtime+size staleness sweep (measured 8 ms) with refreshFromFileSystem(); stale-loud meta field; CLI stays the cold path (inv 21 safe). Closes PRODUCT inv 28 + OQ-R4 simultaneously. Plus the independent importers algorithmic fix. Rejected: skipFileDependencyResolution (silently drops 60 dependency-resolved files — all of scripts/).
  • id-191 scope disposition table in research/fixCache.md (a,b drop; c,e,f reinterpret; d splits).

7. callees + fixture-uses — implementation-ready (full: research/fixQueries.md)

Section titled “7. callees + fixture-uses — implementation-ready (full: research/fixQueries.md)”

Both designed to fixture/test level (row types, algorithms, fixture corpora 19-callees / 20-fixture-uses, exact test lists). Machinery exists: flow-trace’s descendIntoCallee is the proven callee-resolution path; yaml + tinyglobby are already declared deps. Root tsconfig excludes scripts/supabase → fixture-uses needs ad-hoc single-file parses for database.types.ts + scripts/tests/fixtures.

Decisions adopted (proposed defaults, flagged for Liam ratification):

#DecisionAdopted default
D1”fixture by convention”/fixtures/ path segment OR *-fixture.ts basename (both attested in-repo)
D2docs/ontology/*.md no longer exists in-repokeep glob as harmless no-op + document drift; NO private-docs scan (inv 30/16)
D3database.types.ts kind mappingPropertySignature names → key; string-literal union members → value
D4external callees (console.log, .map, supabase methods)excluded by default + top-level externalCount + --include-external emitting callee.file: null; never emit node_modules paths (inv 16)
D5new ErrorKind not_callableadditive enum extension (flow-trace precedent)

8. Schema-coverage report — capability list (dependency order)

Section titled “8. Schema-coverage report — capability list (dependency order)”

From research/baseline.md recommendations; this is the {375.8} scope:

  1. Schema enumeration + existence validation from database.types.ts (unknown_table/unknown_column structured errors — kills silent 0/0).
  2. One-pass all-columns scan — single corpus walk indexing every .from() chain for the whole schema (~one query’s cost vs 1,614 CLI calls).
  3. One-hop const table-name resolution for .from(X) args (converts the 3 observed false “unwired” verdicts; unresolvable sites → per-table “unattributable access” flag, not dropped).
  4. Verdict semantics: wired / read-only / write-only / undecidable (only wildcard/indirect evidence) / unwired (0 rows AND no wildcard/indirect/unattributable smoke on the table). Indirect/wildcard NEVER count as wiring evidence.
  5. Cross-language union: invoke ast-dataflow-py per pass, merge with source: ts|py; recommend installing sqlglot.
  6. SQL-opaque caveat per table (62 RPC bodies + api views are invisible — report the blindness rather than a false verdict). Full RPC→table SQL parsing = future work, NOT this wave.
  7. Owner-facing report output — reuse the type-drift-detect pattern: Markdown summary + classified sections + JSONL backing + --ci baseline.
  8. Cold-start amortisation: solved for the report by one-pass design; for interactive use by the MCP server (§6).
  9. Optional: Tables<'x'> typed-but-never-queried cross-check.

9. Implementation sequencing (feeds the impl workflow)

Section titled “9. Implementation sequencing (feeds the impl workflow)”

Wave 1: truncation helper + integration (foundation, big test surface). Wave 2 (parallel, disjoint files): detectIsTyped fix | callees | importers algorithmic fix. Wave 3 (parallel): fixture-uses | one-hop .from(CONST) resolution. Wave 4: schema-coverage report. Wave 5: dispatch extraction + MCP server (cli.ts refactor last, after all switch cases settle). Wave 6: verify + spec/ROADMAP/skill sync (incl. purging dropped-table examples bid_questions/content_items from cli.ts catalogue + skills).

  • importers warm-state P-19 breach is an algorithmic defect independent of caching (fixed Wave 2).
  • PRODUCT inv 5’s promised RPC return-type fields + row destructuring detection: unimplemented; RPC param heuristic never fires on p_* convention. Left for a future RPC wave alongside fn→table mapping.
  • api-schema views (63) make runtime access resolve via api.*; same-name 1:1 coincidence currently saves attribution. Future: view-aware mapping.
  • Stale smoke-of-record in ROADMAP.md:60 (bid_questions) — sync in Wave 6.