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}.
1. Verdict
Section titled “1. Verdict”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.
2. Market scan (full: research/market.md)
Section titled “2. Market scan (full: research/market.md)”- 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> auditCLI 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 asupa-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:
| Channel | Example | Root cause |
|---|---|---|
.from(TABLE_CONST) variable table args | signup_policy.allowed_domain (a fail-closed auth gate!) reported 0/0 | findFromCalls matches only string literals; ~22 dynamic sites acknowledged in lib/supabase/schema.ts |
| SQL function bodies | corpus_writer_fence_lease.holder_token | 62 RPCs invisible to TS and PY scanners |
| Python pipeline | content_propagation_version.payload_checksum (also a const-table case) | separate CLI, no result union; sqlglot not installed → regex fallback |
| RPC param heuristic never fires | 0 rpc-payload rows in 40 runs | repo 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.ts — truncateSpatial(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-usesis 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:
importersbreaches its 5 s P-19 budget in any state (10.5 s cold / 18-20 s warm — independent algorithmic defect: per-importfindReferencesAsNodes+ 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) withrefreshFromFileSystem(); stale-loudmetafield; CLI stays the cold path (inv 21 safe). Closes PRODUCT inv 28 + OQ-R4 simultaneously. Plus the independentimportersalgorithmic fix. Rejected:skipFileDependencyResolution(silently drops 60 dependency-resolved files — all ofscripts/). - 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):
| # | Decision | Adopted default |
|---|---|---|
| D1 | ”fixture by convention” | /fixtures/ path segment OR *-fixture.ts basename (both attested in-repo) |
| D2 | docs/ontology/*.md no longer exists in-repo | keep glob as harmless no-op + document drift; NO private-docs scan (inv 30/16) |
| D3 | database.types.ts kind mapping | PropertySignature names → key; string-literal union members → value |
| D4 | external 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) |
| D5 | new ErrorKind not_callable | additive 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:
- Schema enumeration + existence validation from
database.types.ts(unknown_table/unknown_columnstructured errors — kills silent 0/0). - 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). - 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). - 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. - Cross-language union: invoke ast-dataflow-py per pass, merge with
source: ts|py; recommend installing sqlglot. - 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.
- Owner-facing report output — reuse the type-drift-detect pattern:
Markdown summary + classified sections + JSONL backing +
--cibaseline. - Cold-start amortisation: solved for the report by one-pass design; for interactive use by the MCP server (§6).
- 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).
10. Out-of-scope findings logged
Section titled “10. Out-of-scope findings logged”importerswarm-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.