Skip to content

R-WP10 — Framework-agnostic adaptation feasibility brief

R-WP10 — Framework-agnostic adaptation feasibility brief

Section titled “R-WP10 — Framework-agnostic adaptation feasibility brief”

Status: INVESTIGATION — S7 (18/05/2026) Authored by: kh-ast-S7 workflow executor Scope: Evaluate what structural changes would be needed to make the ast-dataflow tool portable beyond Next.js; assess the Vite use case specifically. Not in scope: Implementation of any abstraction or adaptor.


The ROADMAP.md Wave 3 row for R-WP10 reads:

Investigate what would be required to adapt the tool to be framework agnostic. Immediate requirement beyond Next.js is Vite. Allows dogfooding across our other non-KH projects.

The tool currently runs against the Knowledge Hub corpus, which is a Next.js 16 (App Router) project. Liam and Claude also build non-KH projects — in particular Vite-based apps — and the cross-project leverage pitch in the ROADMAP introduction (same Supabase + Next.js stack is shared across Knowledge Hub, sales-proposals, and other Liam-and-Claude projects) motivates asking whether the ast-dataflow tool is genuinely portable or has invisible Next.js coupling baked in.

This brief answers four questions posed by R-WP10:

  1. Where does the current implementation assume Next.js? (with file paths and line ranges)
  2. What changes when the target is a Vite project? (the delta)
  3. What abstraction surface would bridge the gap? (three candidate approaches compared)
  4. How much effort would the work cost? (banded estimate with rationale)

1. Current Next.js assumptions (file paths and line ranges)

Section titled “1. Current Next.js assumptions (file paths and line ranges)”

The following survey examined every file in lib/ast-dataflow/ and scripts/ast-dataflow-cli.ts. The conclusion is deliberately nuanced: the query library itself has almost no Next.js coupling, but the project bootstrap has two hard Next.js assumptions that would silently break on a non-Next project, and there are three soft assumptions in the corpus scope definition that Vite projects violate.

1.1 Hard assumption — tsconfig.json plugins: [{ "name": "next" }]

Section titled “1.1 Hard assumption — tsconfig.json plugins: [{ "name": "next" }]”

File: tsconfig.json lines 16–20 (project root)

"plugins": [
{
"name": "next"
}
],

File: scripts/ast-dataflow-cli.ts lines 204–206

const repoRoot = process.cwd();
const tsConfigFilePath = resolve(repoRoot, 'tsconfig.json');
const { project } = createProject({ tsConfigFilePath, repoRoot });

File: lib/ast-dataflow/index.ts lines 47–53

export function createProject(opts: CreateProjectOptions): AstProject {
const project = new Project({
tsConfigFilePath: opts.tsConfigFilePath,
skipAddingFilesFromTsConfig: false,
});
const repoRoot = opts.repoRoot ?? resolve(opts.tsConfigFilePath, '..');
return { project, repoRoot };
}

The CLI hard-codes process.cwd() + /tsconfig.json as the project root and passes it directly into ts-morph’s Project constructor without any pre-processing. ts-morph in turn passes the plugins array to the TypeScript compiler, which ignores unknown plugins (they only affect IDE language service), so the Next.js plugin entry does not break ts-morph. However, the KH tsconfig.json has a side effect of the plugin: it also includes next-env.d.ts in its include array (line 25 of tsconfig.json).

"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
".next/dev/types/**/*.ts"
]

The .next/types/**/*.ts and .next/dev/types/**/*.ts globs reference the .next/ build output directory, which is present during dev/build but not in a clean checkout. ts-morph silently tolerates missing glob expansions, so this is not a crash risk, but it means the corpus boundary is implicitly Next.js-aware. A Vite project would have no .next/ but might have .vite/ or dist/ instead — these are inert for the ast-dataflow tool because generated output is typically excluded from tsconfig, but the assumption is worth naming.

Verdict: The plugins: next entry is a non-issue for ts-morph but the .next/types include globs embed a Next.js file system assumption. When the CLI is run against a Vite project’s tsconfig.json, these globs simply produce no files; the tool does not break. However, this is accidental portability, not designed portability — a future Next.js version that requires the plugin entries for correct type resolution would break the tool’s “discover corpus from tsconfig” contract.


1.2 Hard assumption — @/ path alias in scripts/ast-dataflow-cli.ts

Section titled “1.2 Hard assumption — @/ path alias in scripts/ast-dataflow-cli.ts”

File: scripts/ast-dataflow-cli.ts lines 14–16

import {
callers, columnReads, columnWrites, deadExports, enumUses,
importers, reexportChain, references, stringLiteralUses,
typeEvolution, createProject,
} from '@/lib/ast-dataflow';
import type { ReferenceKind } from '@/lib/ast-dataflow';

The CLI script uses @/lib/ast-dataflow as the import path. This resolves correctly because the KH tsconfig.json defines "paths": { "@/*": ["./*"] }. The CLI is executed via bun scripts/ast-dataflow-cli.ts, and Bun reads the tsconfig path mapping for module resolution.

A Vite project’s tsconfig will almost certainly have a different path alias convention. Vite projects typically define path aliases in vite.config.ts (e.g. resolve: { alias: { '@': '/src' } }) and mirror them in tsconfig as "paths": { "@/*": ["./src/*"] }. This means @/lib/ast-dataflow would resolve to src/lib/ast-dataflow rather than lib/ast-dataflow. The CLI script itself would fail to import the library if run from a Vite project root.

Verdict: This is the single genuine hard coupling in the codebase. The CLI script cannot be dropped into a Vite project and executed as-is because its @/ imports assume KH’s root-anchored path alias. The fix is either: (a) rewrite the CLI to use relative imports (import ... from '../lib/ast-dataflow'), or (b) document that the CLI must be run from the ast-dataflow-tooling root rather than the target project root — which is actually already how it works (the CLI is run from the ast-dataflow worktree, querying a different project’s tsconfig via the --tsconfig flag or process.cwd()). Option (b) is discussed further in §3.


1.3 Hard assumption — isTestFilePath pattern embeds KH test directory name

Section titled “1.3 Hard assumption — isTestFilePath pattern embeds KH test directory name”

File: lib/ast-dataflow/resolve.ts lines 288–297

export function isTestFilePath(relPath: string): boolean {
return (
relPath.startsWith('__tests__/') ||
relPath.includes('/test/') ||
relPath.endsWith('.test.ts') ||
relPath.endsWith('.test.tsx') ||
relPath.endsWith('.spec.ts') ||
relPath.endsWith('.spec.tsx')
);
}

Also duplicated in:

  • lib/ast-dataflow/queries/column-reads.ts lines 271–279 (local isTestFile)
  • lib/ast-dataflow/queries/column-writes.ts lines 280–288 (local isTestFile)

The function hard-codes __tests__/ as the primary test directory prefix, which is the KH convention. Vite projects typically use src/__tests__/, src/tests/, tests/, or co-located .test.ts files. The .test.ts / .spec.ts suffix check covers the common Vitest/Jest pattern and is portable. However:

  • relPath.startsWith('__tests__/') — misses src/__tests__/ (common in Vite scaffolded projects using create-vite or Vitest defaults)
  • The local copies in column-reads.ts and column-writes.ts diverge from the shared resolve.ts version, creating a maintenance risk even within the KH corpus

Verdict: Soft coupling. The .test.ts / .spec.ts suffix checks cover 90% of Vitest projects. The __tests__/ prefix is a KH-specific path assumption that would produce false negatives (test files at src/__tests__/ would be classified as production files) when using --exclude-tests against Vite projects. Low practical impact unless the target Vite project uses __tests__/-at-root.


1.4 Soft assumption — supabase-js call-chain heuristic in column-reads and column-writes

Section titled “1.4 Soft assumption — supabase-js call-chain heuristic in column-reads and column-writes”

File: lib/ast-dataflow/queries/column-reads.ts lines 214–237 (findFromCalls) File: lib/ast-dataflow/queries/column-reads.ts lines 239–268 (findRpcCalls) File: lib/ast-dataflow/queries/column-writes.ts (parallel implementation)

The column-reads and column-writes queries are fundamentally Supabase- specific: they scan for .from('table').select(...), .insert(...), .update(...), .upsert(...), and .rpc(...) call chains. These are supabase-js method names, not Next.js method names.

These queries are not less portable on Vite than on Next.js. Any project using supabase-js — regardless of framework — will benefit from them unchanged. The queries are correctly scoped as “Supabase queries”, not “Next.js route queries”. The Supabase from().select() pattern is identical whether the call site is in a Next.js route handler or a Vite React component.

Verdict: Not a portability concern for Vite. The queries work correctly against any TypeScript corpus that uses supabase-js.


1.5 Soft assumption — jsxProp classification assumes JSX is present

Section titled “1.5 Soft assumption — jsxProp classification assumes JSX is present”

File: lib/ast-dataflow/queries/string-literal-uses.ts lines 35–49

// Rule 2: JSX attribute value — `<Comp prop="value" />`
if (parentKind === SyntaxKind.JsxAttribute) {
return 'jsxProp';
}
// Check grandparent for the JsxAttribute case where the parent IS a JsxExpression wrapper
if (grandParent) {
const gpKind = grandParent.getKind();
if (gpKind === SyntaxKind.JsxAttribute) {
return 'jsxProp';
}
}

The string-literal-uses query classifies string literals inside JSX attributes as 'jsxProp'. This only fires when the project actually contains .tsx files with JSX. Vite React projects use .tsx extensively — the classification is more likely to be exercised on a Vite React project, not less.

The KH tsconfig.json sets "jsx": "react-jsx". A Vite project may set "jsx": "react-jsx" (React projects) or "jsx": "preserve" (Vue or raw Vite) or omit JSX entirely (Node.js Vite projects). If the target project omits JSX, JsxAttribute nodes will never appear in the AST, and the jsxProp classification will simply never fire — the query degrades gracefully rather than breaking.

Verdict: Not a portability concern. The JSX classification is additive; its absence from non-JSX projects is benign.


1.6 Soft assumption — dead-exports test-file detection has the same __tests__/ root coupling

Section titled “1.6 Soft assumption — dead-exports test-file detection has the same __tests__/ root coupling”

File: lib/ast-dataflow/resolve.ts lines 288–297 (shared isTestFilePath, called by dead-exports, reexport-chain, walkBarrelChain)

Same observation as §1.3 — the __tests__/ prefix check in isTestFilePath is the KH convention. For Vite projects with tests co-located at src/, the --exclude-tests flag would be less precise but would not crash. Files at src/__tests__/ would not be excluded as tests unless the .test.ts suffix catches them.


AssumptionFilesSeverityBreaks on Vite?
tsconfig.json plugins: next entry consumed by CLIscripts/ast-dataflow-cli.ts:205, tsconfig.json:16–20LowNo (ts-morph ignores TS plugins)
.next/types include globs in tsconfig corpus boundarytsconfig.json:25LowNo (silent empty expansion)
@/lib/ast-dataflow import in CLI scriptscripts/ast-dataflow-cli.ts:14–16HIGHYes — if CLI is copied to target project root
isTestFilePath __tests__/ root prefixresolve.ts:289, column-reads.ts:273, column-writes.ts:282LowPartial (.test.ts suffix catches most Vitest files)
supabase-js chain heuristicscolumn-reads.ts, column-writes.tsN/AN/A (Supabase is framework-agnostic)
jsxProp classificationstring-literal-uses.ts:35–49LowNo (degrades gracefully without JSX)
dead-exports test detectionresolve.ts:288–297, reexport-chain.ts:209LowPartial (same as isTestFilePath)

This section characterises the meaningful differences between a Next.js 16 project and a Vite project from the perspective of the ast-dataflow tool.

PropertyKH (Next.js)Typical Vite project
moduleResolution"bundler""bundler" (Vite 5+) or "node" (older)
jsx"react-jsx""react-jsx" (Vite React) or "preserve" or absent
paths{ "@/*": ["./*"] }{ "@/*": ["./src/*"] } or { "~/*": ["./src/*"] } or custom
baseUrlabsent (implicit .)sometimes "." or "./src"
plugins[{ "name": "next" }]absent (Vite manages its own config)
include["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"]["src/**/*.ts", "src/**/*.tsx"] or ["**/*.ts", "**/*.tsx"]
exclude["node_modules", "scripts", "supabase", "mcp-apps"]["node_modules", "dist"]

Key difference for the tool: The paths mapping anchors the @/ alias at the project root (KH) vs at ./src/ (Vite). The include globs define the indexed corpus. Since ts-morph reads the tsconfig directly and uses the TypeScript compiler’s own module resolver, both cases are handled correctly by ts-morph with zero code changes, provided the correct tsconfig.json is passed.

The tool’s createProject({ tsConfigFilePath }) will index the files specified in any valid tsconfig. The module-resolution differences between "bundler" and "node" are handled transparently by the TypeScript compiler embedded in ts-morph.

ConventionNext.js App Router (KH)Vite React
Entry pointapp/layout.tsx + app/page.tsxsrc/main.tsx
Route filesapp/**/route.ts (GET, POST exports)src/pages/ (if using React Router) or no file-based routing
API routesapp/api/**/route.tsSeparate Express/Hono server, or Vite plugin, or serverless function
Config filenext.config.tsvite.config.ts
Test directory__tests__/ (KH convention)src/__tests__/ or co-located .test.ts
Type augmentationnext-env.d.tsNone (or framework-specific)

Impact on queries:

  • callers, references, importers: These queries start from a user- supplied symbol and use ts-morph’s findReferences(). They are entirely indifferent to the file system layout. A call to callers('src/utils/format.ts:formatDate') works identically on a Vite project.

  • column-reads, column-writes: Walk the entire corpus looking for .from('table') call chains. The file system layout is irrelevant — the queries find these calls wherever they appear.

  • dead-exports: Scans the corpus for exported symbols with zero external importers. The scanned corpus is defined by the tsconfig include/exclude. A Vite project’s corpus lives under src/ — ts-morph will index it correctly from the Vite tsconfig.

  • string-literal-uses: Pure text + AST walk. No file system assumptions.

  • importers: The resolveTargetFilePath function in lib/ast-dataflow/queries/importers.ts lines 28–79 uses a clever approach: it scans all import declarations across the corpus and matches specifiers against the input. The fallback strip of @/ prefix at line 60:

    if (resolvedNormalised.endsWith('/' + normalised.replace(/^@\//, ''))) {

    strips the @/ prefix to match against the resolved absolute path. This works for KH’s @/ alias but may fail for a Vite project using ~/ or a custom prefix. The replace(/^@\//, '') is KH-specific.

Next.js uses proxy.ts (project root) as an auth middleware that runs on every request via the matcher configuration. The ast-dataflow tool does not inspect proxy.ts or the publicRoutes allowlist. It has no concept of middleware. Vite delta: zero. The proxy is irrelevant to the AST tool.

Next.js Dev Server and Vite’s HMR model are irrelevant — the ast-dataflow tool is a CLI that runs offline against a static file system snapshot. It does not connect to a dev server, does not watch for file changes, and does not interact with the build pipeline. Vite delta: zero.

Next.js (App Router, moduleResolution: "bundler") and Vite 5 (also moduleResolution: "bundler") use the same TypeScript module resolution mode. Older Vite 4 projects may use "node" or "node16". ts-morph respects whichever mode is configured in the tsconfig, so the resolution works correctly in all three cases without tool modification.

One nuance: Vite’s runtime module resolver (its own bundler) and TypeScript’s "bundler" mode have subtle differences with exports field in package.json. These differences only surface when a library exports type-only subpath packages. In practice, the ast-dataflow tool does not resolve packages — it only resolves source files within the indexed corpus — so this distinction is irrelevant.

Vite projects introduce some file patterns the tool would encounter but handle correctly:

PatternExampleTool impact
vite.config.tsimport { defineConfig } from 'vite'Indexed as a normal TS file; dead-exports may flag defineConfig as external (non-importable) — but it is a package import, not a project export, so it will never appear as an “exported symbol”
index.htmlVite entry pointNot a TS file; not in corpus
CSS modulesimport styles from './App.module.css'ts-morph treats .css as non-TS; the import is visible as a string literal but the module is not indexed
.vue / .svelte filesNon-TS component formatsNot included in tsconfig include (unless a preprocessor plugin is configured); not indexed
DimensionDelta from Next.jsTool action needed
tsconfig corpussrc/** instead of **None — createProject reads any tsconfig
path alias@/src/ instead of @/Minor: fix importers.ts line 60 strip
Route conventionsNo app/**/route.tsNo route-aware logic in tool; irrelevant
Proxy / middlewareAbsentIrrelevant
HMR / dev serverDifferentIrrelevant
Test directorysrc/__tests__/ commonMinor: generalise isTestFilePath
JSX handlingSame (react-jsx) for React ViteNone
Module resolution mode"bundler" (Vite 5) or "node" (Vite 4)None — ts-morph respects the setting

Three approaches are evaluated. They are ordered from least to most architectural work.


Approach A — Minimal: fix two concrete issues, document the rest

Section titled “Approach A — Minimal: fix two concrete issues, document the rest”

Description: Repair the two identified defects (@/ alias strip in importers.ts and duplicated/fragile isTestFilePath) and update the CLI to accept an explicit --tsconfig <path> flag. No framework adaptor abstraction.

Changes required:

  1. scripts/ast-dataflow-cli.ts — add --tsconfig flag (lines 204–206)

    Replace the hard-coded resolve(process.cwd(), 'tsconfig.json') with:

    const tsConfigFilePath = typeof parsed.flags.tsconfig === 'string'
    ? resolve(process.cwd(), parsed.flags.tsconfig)
    : resolve(process.cwd(), 'tsconfig.json');

    This allows invoking the tool against a remote project:

    Terminal window
    cd /path/to/ast-dataflow-tooling
    bun scripts/ast-dataflow-cli.ts callers \
    --tsconfig /path/to/vite-project/tsconfig.json \
    --symbol 'src/utils/format.ts:formatDate'
  2. lib/ast-dataflow/queries/importers.ts line 60 — generalise the alias strip

    Change:

    if (resolvedNormalised.endsWith('/' + normalised.replace(/^@\//, ''))) {

    To:

    const strippedAlias = normalised.replace(/^[^/]+\//, ''); // strip any leading alias
    if (resolvedNormalised.endsWith('/' + strippedAlias)) {

    This handles @/, ~/, #/, and other single-segment alias prefixes.

  3. lib/ast-dataflow/resolve.ts — generalise isTestFilePath (lines 288–297)

    Promote __tests__/ to a configurable default; add src/__tests__/:

    const TEST_DIR_PREFIXES = ['__tests__/', 'src/__tests__/'];
    const TEST_SUFFIX_PATTERNS = ['.test.ts', '.test.tsx', '.spec.ts', '.spec.tsx'];
    export function isTestFilePath(relPath: string): boolean {
    return (
    TEST_DIR_PREFIXES.some(p => relPath.startsWith(p)) ||
    relPath.includes('/test/') ||
    TEST_SUFFIX_PATTERNS.some(s => relPath.endsWith(s))
    );
    }

    Also eliminate the two local copies in column-reads.ts and column-writes.ts that duplicate this logic.

  4. scripts/ast-dataflow-cli.ts — fix @/lib/ast-dataflow import (lines 14–16)

    The CLI’s own imports must use relative paths so the CLI can be executed from a different working directory without the @/ resolution breaking:

    import { callers, ... } from '../lib/ast-dataflow';

    Currently this works when Bun resolves the tsconfig from process.cwd(). When --tsconfig points at a remote project, process.cwd() may still be the ast-dataflow-tooling root, so the current setup is actually safe. However, the relative import makes the invariant explicit and removes the dependency on the @/ path alias being present in the active tsconfig.

Trade-offs:

ProCon
Minimal change surface — 4 files touchedNo formal framework contract; each new framework requires another ad-hoc audit
No new abstraction to learn or maintainisTestFilePath still hard-codes common-but-not-universal directory names
--tsconfig flag is the natural extension point; already how advanced CLI tools work (e.g. tsc --project)Callers must supply the tsconfig path explicitly for non-KH projects
Vite support is achieved with zero new interfacesDocs must explain the --tsconfig pattern rather than advertising “framework-agnostic”

Conclusion: This approach is sufficient for 95% of the Vite use case described in R-WP10 and requires no architectural change. It is the recommended starting point.


Approach B — Moderate: FrameworkAdaptor interface

Section titled “Approach B — Moderate: FrameworkAdaptor interface”

Description: Define a FrameworkAdaptor interface that encapsulates the framework-specific behaviours the tool currently hard-codes, then ship two concrete implementations: NextjsAdaptor (default) and ViteAdaptor.

Proposed interface shape:

lib/ast-dataflow/adaptors/framework-adaptor.ts
export interface FrameworkAdaptor {
/**
* Return the corpus tsconfig.json path given a repo root.
* Default: `${repoRoot}/tsconfig.json`
*/
resolveCorpusTsConfig(repoRoot: string): string;
/**
* Return true if the given repo-relative path should be classified
* as a test file (for --exclude-tests purposes).
*/
isTestFile(relPath: string): boolean;
/**
* Strip the framework-specific path alias prefix from a module specifier
* so `importers.ts` can match `@/lib/foo` against `~/lib/foo` etc.
* Default: strip any single-segment alias prefix of the form `alias/`.
*/
stripAliasPrefix(specifier: string): string;
/**
* Auto-detect from the repo root: return the adaptor that best fits
* the detected framework (by probing `next.config.*`, `vite.config.*`, etc.)
* Returns 'unknown' when detection is not conclusive.
*/
detect(repoRoot: string): 'nextjs' | 'vite' | 'unknown';
}

Concrete implementations:

lib/ast-dataflow/adaptors/nextjs.ts
export const NextjsAdaptor: FrameworkAdaptor = {
resolveCorpusTsConfig: (root) => resolve(root, 'tsconfig.json'),
isTestFile: (p) => p.startsWith('__tests__/') || ... (current logic),
stripAliasPrefix: (s) => s.replace(/^@\//, ''),
detect: (root) => existsSync(join(root, 'next.config.ts')) || existsSync(join(root, 'next.config.js')) ? 'nextjs' : 'unknown',
};
// lib/ast-dataflow/adaptors/vite.ts
export const ViteAdaptor: FrameworkAdaptor = {
resolveCorpusTsConfig: (root) => resolve(root, 'tsconfig.json'),
isTestFile: (p) => p.startsWith('src/__tests__/') || p.startsWith('__tests__/') || ...,
stripAliasPrefix: (s) => s.replace(/^[@~#][^/]*\//, ''),
detect: (root) => existsSync(join(root, 'vite.config.ts')) || existsSync(join(root, 'vite.config.js')) ? 'vite' : 'unknown',
};

The createProject factory would accept an optional adaptor:

createProject({ tsConfigFilePath, repoRoot, adaptor?: FrameworkAdaptor })

Auto-detection would probe next.config.* / vite.config.* at the repo root and select the matching adaptor.

Trade-offs:

ProCon
Explicit, testable contract for framework-specific behaviourTwo new files + interface; more ongoing maintenance
Auto-detection removes the need for callers to specify --frameworkDetection by config file presence is fragile (monorepos, custom project structures)
Each adaptor is independently testable with a fixture corpusThe interface is likely over-engineered for a two-framework world
Clean surface for a future SvelteKitAdaptor etc.All current tool users get a hidden NextjsAdaptor dependency even when they’re on KH and don’t need the abstraction

Conclusion: Useful if the tool is expected to actively support multiple frameworks in parallel (a “publish as a package” future). Premature for the current dogfooding-within-Liam-projects use case.


Approach C — Configuration-driven: ast-dataflow.config.ts

Section titled “Approach C — Configuration-driven: ast-dataflow.config.ts”

Description: Introduce a per-project configuration file at the repo root that overrides the default behaviours without requiring code changes to the tool. The tool reads this file at startup and applies its settings.

Proposed config shape:

// ast-dataflow.config.ts (at target project root)
import type { AstDataflowConfig } from 'ast-dataflow-tooling/config';
export default {
tsconfig: './tsconfig.json', // default
testDirectories: ['__tests__', 'tests'], // default: ['__tests__']
testFileSuffixes: ['.test.ts', '.test.tsx', '.spec.ts', '.spec.tsx'],
aliasPrefix: '@/', // default; set to '~/' for Vite projects using tilde alias
} satisfies AstDataflowConfig;

The CLI would discover ast-dataflow.config.ts (or .js) via a search up from process.cwd(), similar to how ESLint, Prettier, and Vitest discover their configs.

Trade-offs:

ProCon
No new abstraction in library code; config is user-facing onlyDiscovery logic adds complexity and a new dependency pattern (dynamic import of a TS config file)
Follows modern tooling conventions (ESLint flat config, Vitest config)Adds a new file to the target project’s repo — more friction than --tsconfig flag
Config file is checked in and self-documentingThe config schema must be versioned and maintained
Extensible beyond framework-agnostic: could configure corpus limits, alias rules, etc.Most of the config options duplicate tsconfig semantics — two sources of truth

Conclusion: The best approach if the tool is eventually published as an npm package for general use. Too heavy for the current single-operator, single-codebase context.


CriterionA (Minimal)B (Adaptor)C (Config file)
Files added0~4~2
Files modified464 + optional CLI changes
New interfaces011 (config schema)
Vite support achieved?YesYesYes
Future-framework supportAd-hoc auditPlugin a new adaptorAdd config defaults
Effort (total)SMS–M
Right-sizes for current use case?YesNoNo

Approach A (Recommended): S — 2–4 hours

The four changes described in §3 Approach A are each small and well-isolated:

  1. --tsconfig CLI flag: ~30 lines diff in scripts/ast-dataflow-cli.ts.
  2. Alias strip generalisation: 3-line diff in lib/ast-dataflow/queries/importers.ts.
  3. isTestFilePath generalisation + deduplication: ~15-line diff in lib/ast-dataflow/resolve.ts + removal of two local copies.
  4. Relative imports in CLI: ~3-line diff.

Each change is independently testable. No new test infrastructure needed. Existing tests (importers.test.ts, dead-exports tests) provide a regression baseline. A smoke run against a local Vite project fixture is the verification.

Approach B (Adaptor pattern): M — 1–2 weeks

Designing a stable FrameworkAdaptor interface requires:

  • Deciding which behaviours belong in the adaptor vs stay in the library core.
  • Writing NextjsAdaptor and ViteAdaptor with full test coverage.
  • Threading the adaptor through all query callers that use isTestFilePath and the alias strip.
  • Writing auto-detection logic and deciding the fallback.
  • Updating createProject signature and all call sites.

The interface design itself (OQ phase) could easily consume a full session. One week is the optimistic estimate; two weeks if the interface surfaces edge cases that require iteration.

Approach C (Config file): S–M — 4–8 hours

Simpler than Approach B but requires:

  • Writing a config discovery function (search up from process.cwd()).
  • Defining and exporting a typed config schema.
  • Integrating config loading into the CLI startup.
  • Deciding what happens when no config file is present (sensible defaults).

The main risk is config file discovery interacting badly with monorepos or worktree layouts — requires at least one explicit test case.


4b. Vite adaptor on top of the abstraction work

Section titled “4b. Vite adaptor on top of the abstraction work”

On top of Approach A: XS — 1–2 hours

After Approach A, Vite support requires:

  • A quick smoke run against a real Vite project (e.g. a create-vite scaffold with a few TypeScript files).
  • Verifying the --tsconfig flag correctly points ts-morph at the Vite tsconfig.
  • Checking that isTestFilePath covers the Vite test layout.
  • Documenting the invocation pattern in the skill file (R-WP7).

Total: approximately 1–2 hours for validation plus documentation.

On top of Approach B: S — 3–5 hours

Writing ViteAdaptor, its tests, and wiring it into the auto-detection path. The adaptor itself is small (4 methods); the work is mostly test scaffolding.

On top of Approach C: S — 2–3 hours

Writing a sample ast-dataflow.config.ts for a Vite project, verifying discovery works, adding a test.


Ship Approach A in S8 (or as an S7 sub-task).

Rationale:

  1. The problem is smaller than it appears. The tool has almost no Next.js coupling in its query logic. The two genuine defects (the @/ alias strip in importers.ts and the isTestFilePath duplication) are each 3–15 line fixes. The --tsconfig flag is a natural extension that most advanced CLI tools already expose.

  2. Approach A achieves the stated goal. R-WP10 says “immediate requirement beyond Next.js is Vite.” Approach A delivers usable Vite support. The invocation becomes:

    Terminal window
    bun scripts/ast-dataflow-cli.ts callers \
    --tsconfig /path/to/vite-project/tsconfig.json \
    --symbol 'src/utils/format.ts:formatDate'

    This is identical to how tsc and other tools work and requires no new abstraction.

  3. Abstraction layers are premature. The tool currently serves one operator (Liam and Claude agents on KH and Liam’s other projects). Approaches B and C trade present engineering investment for a future that may never require it. The ROADMAP explicitly defers the npm-packaging question (R-WP7 is a skill file, not a published package).

  4. Deduplication of isTestFilePath is valuable independently of framework portability. The three copies (resolve.ts, column-reads.ts, column-writes.ts) are a maintenance risk on KH itself — they drift. This fix belongs in a housekeeping commit regardless of the portability goal.

  5. The only non-trivial risk is the repoRoot / cwd interaction. When the --tsconfig flag points at a remote project, repoRoot (used by all path helpers to produce repo-relative results) must match the target project root, not the ast-dataflow-tooling root. This is a one-line change (repoRoot = dirname(tsConfigFilePath)), but it must be clearly documented and tested to avoid subtly wrong result paths.

If R-WP10 is scheduled alongside R-WP6 (flow-trace) in S8, Approach A is a single short work package. If it is promoted to a standalone S7 WP, it can be delivered in a single agent session.


OQQuestionOwnerWhen
OQ-WP10-1Should the --tsconfig flag accept an absolute path, a repo-relative path, or both? Current resolve(process.cwd(), flag) handles relative + absolute (via isAbsolute check), which is the standard CLI convention.S8 executorBefore implementing
OQ-WP10-2When --tsconfig points at a remote project, should repoRoot default to dirname(tsConfigFilePath) or be separately configurable via --repo-root? The former is simpler; the latter handles monorepos where the tsconfig is not at the true project root.S8 executorBefore implementing
OQ-WP10-3Should the isTestFilePath generalisation be a PRODUCT.md invariant (making the configurable test directory list part of the spec) or an internal implementation detail? The spec currently says nothing about test directory conventions.OrchestratorBefore PRODUCT.md update
OQ-WP10-4Does the importers.ts alias strip change (stripping any single-segment prefix) risk matching legitimate path fragments that happen to look like aliases (e.g. api/foo)? The current regex replace(/^[^/]+\//, '') would strip api/ from api/foo. Should it be limited to known alias characters (@, ~, #)?S8 executorImplementation detail
OQ-WP10-5Is there a real Vite project already in Liam’s ecosystem that should be used as a smoke-test corpus? The brief assumes a create-vite scaffold; a real project would give more signal on edge cases.LiamBefore S8 smoke test
OQ-WP10-6R-WP10 says “Immediate requirement beyond Next.js is Vite.” Is this an active need (Liam has a Vite project he wants to query today) or a strategic hedge (may be needed within the next 2-3 sessions)? The answer affects whether Approach A lands in S7 or S8.LiamBefore S8 planning

End of brief.