id-375 research — fixQueries
fixQueries
Section titled “fixQueries”Summary
Section titled “Summary”Both unshipped PRODUCT invariants are implementable with existing machinery and one already-declared dependency each. (A) callees (inv 2) is a body-descendant walk: resolveSymbol() (resolve.ts:72) finds the declaration, getBody().getDescendantsOfKind(CallExpression|NewExpression) enumerates calls (nested closures included for free), and the callee is resolved via expr.getSymbol()?.getAliasedSymbol() ?? getSymbol() exactly as flow-trace’s descendIntoCallee already does (flow-trace.ts:409-421); the existing CallResolution union (types.ts:3-9) already covers direct/aliased/destructured/computed-property/indirect. The one real design decision is out-of-corpus callees (console.log, Array.map, supabase .d.ts methods) which collide with inv 16’s “never emit paths outside the corpus” — recommend excluding by default behind --include-external with callee.file: null. (B) fixture-uses (inv 11) is a three-mode scanner outside the ts-morph project (root tsconfig.json:66-74 excludes scripts, supabase, and the tool’s own fixtures, so a project walk cannot work): a hand-rolled JSON string-token lexer for key-vs-value with offset→line mapping (no jsonc-parser in node_modules; stream-json also absent), ad-hoc single-file ts-morph parses for fixture-flagged TS + database.types.ts (PropertySignature names = kind ‘key’), and the yaml package (already a devDependency, already imported by scripts/sync-intent-notes.ts:68) via parseDocument for md frontmatter with real node ranges. File discovery via tinyglobby (declared devDependency, package.json:197). Three spec ambiguities need owner decisions, the biggest being that docs/ontology/*.md no longer exists in the canonical repo — the ontology docs moved to the private docs-site, which inv 30 forbids scanning.
Findings
Section titled “Findings”Grounding evidence
Section titled “Grounding evidence”Spec state
Section titled “Spec state”- PRODUCT.md inv 2 (callees) and inv 11 (fixture-uses) are the two unshipped query invariants: ROADMAP.md:41-45 and the DEFERRED table at ROADMAP.md:73-74 confirm both are deferred with no load-bearing trigger. (ROADMAP.md:75 also lists inv 12, but
enum-usesshipped as the covering ROADMAP extension — ROADMAP.md:66.) - TECH.md already sketches both: TECH.md:261 (
callees=getBody()?.getDescendantsOfKind(SyntaxKind.CallExpression)→getExpression().getSymbol(),resolution: 'indirect'when no symbol resolves) and TECH.md:270 (fixture-uses= two-mode: TS AST walk + streaming JSON parser, “stream-json or hand-rolled”, limited to the fixture globs). Test-plan rows: TECH.md:774 (callees) and TECH.md:783 (fixture-uses). - Governing result-shape invariants: inv 13 (stable JSON schema, 1-based positions), inv 14 (200-row cap +
truncated/total_estimated), inv 15 (confidencetag; fixture grep =indirect), inv 16 (repo-root-relative POSIX paths only, never outside the corpus), inv 19 (callees is resolution-class: 5 s P95 warm; fixture-uses heuristic-class: 10 s P95), inv 23 (ts-morph parity for callees), inv 24 (no exact-tier false negatives for fixture-uses), inv 29 (structured errors), inv 30 (no writes, no network, worktree-scoped reads).
Reusable machinery (tool source)
Section titled “Reusable machinery (tool source)”tools/ast-dataflow/resolve.ts:72-190—resolveSymbol(project, '<file>:<name>', repoRoot)resolves functions, methods, classes, variables; prefers FunctionDeclaration/MethodDeclaration on name collision; throws typedAstResolverErrorwith kinds fromtypes.ts:43-50. Directly reusable as callees’ entry point.tools/ast-dataflow/queries/flow-trace.ts:395-548—descendIntoCalleealready performs exactly the callee-resolution callees needs:callExpr.getExpression().getType()→getSymbol() ?? getAliasSymbol()→getDeclarations()[0](lines 409-421), then function-like kind check (426-431) andNode.isBodyablebody check (437-443). This is the proven type-checker path for method calls on inferred types.tools/ast-dataflow/queries/callers.ts:18-44—classifyResolution(import-alias detection) andcallers.ts:115-154—findCallExpression(walks up through PropertyAccessExpression/NonNullExpression, treats NewExpression as a call). Both patterns transfer to callees (inverted direction).tools/ast-dataflow/resolve.ts:226-315—findEnclosinggives theenclosingstring for call sites inside nested closures (handles arrow-in-callback, property-assignment methods, constructors).tools/ast-dataflow/types.ts:3-9— existingCallResolutionunion ='direct' | 'reexport' | 'aliased' | 'destructured' | 'computed-property' | 'indirect'— a strict superset of what inv 2 demands; reuse rather than mint a new union.tools/ast-dataflow/queries/string-literal-uses.ts:121-214— corpus-fanout iteration + exactgetLiteralValue()matching +totalEstimated/cap idiom. NOT directly reusable for fixture-uses matching (its classifier deliberately drops object-literal values and non-call contexts, string-literal-uses.ts:116-119), but the response-envelope/cap idiom is the model.tools/ast-dataflow/cli.ts:30-47(flag parser), 107-253 (catalogue), 375-841 (per-query switch cases),parseLimit(65-81),emitResponse(94-105) — the wiring template.index.ts:46-53createProject.
Corpus constraints that shape fixture-uses (critical)
Section titled “Corpus constraints that shape fixture-uses (critical)”- Root
tsconfig.json:51-74: include is**/*.ts/**/*.tsxonly; exclude listsscripts,supabase,mcp-apps,tools/ast-dataflow/__tests__/fixtures. Consequences: (a)supabase/types/database.types.tsandscripts/tests/fixtures/**/*.tsare NOT in the ts-morph project — fixture-uses must load them ad hoc; (b) JSON files are never in the project regardless ofresolveJsonModule: true(tsconfig.json:31) — JSON needs raw-text scanning; (c)__tests__/**ande2e/**TS are in the project. - Real targets verified:
__tests__/**/*.json= 11 files (e.g.__tests__/fixtures/keyword-normalisation-cases.json,__tests__/fixtures/eval-baselines/*.baseline.json);e2e/fixtures/= mixed TS (change-reports-fixture.ts,test-data-fixture.ts), JSON (embeddings.json), and afiles/subdir (non-text assets — must be extension-filtered);scripts/tests/fixtures/= JSON snapshots + subdirs (extraction/,form-extraction/). docs/ontology/does not exist in the canonical repo (repodocs/contains onlyextend-registry-provenance.md,reference/,reports/,testing/). The ontology markdown moved to the PRIVATE docs-site:${KH_PRIVATE_DOCS_DIR}/src/content/docs/ontology/*.md(10+ files, e.g.03-layer-vocabulary.mdwith rich YAML frontmatter:cv_name: layer_vocabulary,baseline_values: [{ key: sales_brief, ... }]). Scanning that path would violate inv 30 (reads scoped to the worktree) and inv 16 (no paths outside the corpus). Spec drift — decision needed.supabase/types/database.types.tsexists (referenced as schema-canonical in CLAUDE.md; the directory is read-restricted in this session but the path is confirmed). Column needles there appear as PropertySignature identifiers (project_id: string) and union string literals — a plain string-literal walk would miss the keys, so TS-mode must match property-name identifiers too.
Dependency audit (for fixture-uses)
Section titled “Dependency audit (for fixture-uses)”package.json:197tinyglobby: ^0.2.16andpackage.json:200yaml: ^2.9.0are declared devDependencies (dev-tool usage is fine — the tool itself runs viabun run ast-dataflow, package.json:50).jsonc-parser: NOT in node_modules.stream-json(TECH.md’s suggestion): not declared.fast-glob: present only transitively — should not be imported directly (repo convention: direct file imports of declared deps only).yamlis already imported directly in-repo:scripts/sync-intent-notes.ts:68(import { parse as parseYaml, stringify as stringifyYaml } from 'yaml').yaml@2’sparseDocumentexposes noderangeoffsets, giving real line numbers for frontmatter matches without regex.- No direct
tinyglobbyimporter exists yet; it is nevertheless a declared dependency and safe to import.
Test/fixture conventions
Section titled “Test/fixture conventions”- Fixture dirs are numbered per query under
tools/ast-dataflow/__tests__/fixtures/(01-callers … 18-latent-fixes); next free slots are 19-callees and 20-fixture-uses. Each dir carries its own minimaltsconfig.json(e.g.01-callers/tsconfig.json: NodeNext, strict,include: ["./*.ts"]). - Test files instantiate the tool against the fixture dir as its own repo root:
string-literal-uses.test.ts:21-28(createProject({ tsConfigFilePath: resolve(FIXTURE_DIR, 'tsconfig.json'), repoRoot: FIXTURE_DIR })) — this pattern lets fixture-uses tests replicate the real repo layout (__tests__/,e2e/fixtures/,supabase/types/) inside the fixture dir. - Vitest picks up
tools/**/*.test.{ts,tsx}(vitest.config.ts:23). Test doctrine: exacttoHaveLengthcounts,toMatchObject, product-spec titles (string-literal-uses.test.ts:5-19; docs/reference/testing/test-philosophy.md).
Spec ambiguities in inv 11 needing decisions
Section titled “Spec ambiguities in inv 11 needing decisions”- “
__tests__/**/*.tsflagged as fixture by path or convention” — “convention” is undefined. Observed repo conventions:/fixtures/path segment (__tests__/fixtures/**,e2e/fixtures/**,scripts/tests/fixtures/**) and-fixture.tsbasename suffix (e2e/fixtures/change-reports-fixture.ts,test-data-fixture.ts). No.fixture.tsdotted variant found. docs/ontology/*.md— target no longer exists in-repo (moved to private docs-site). Options: (a) keep the glob (matches nothing today, future-proof), (b) generalise todocs/**/*.mdfrontmatter, (c) env-gated scan ofKH_PRIVATE_DOCS_DIR— (c) conflicts with inv 30/16.supabase/types/database.types.tsmatch semantics — inv 11 frames key/value in JSON terms; in a generated TS types file the “keys” are PropertySignature identifiers, not string literals. Needs the kind mapping made explicit (proposed: property names →key, string-literal union members →value).- Whether test code string literals belong in fixture-uses — inv 11 says fixtures only;
string-literal-usesalready covers test code (OQ5, PRODUCT.md:418-425). Recommend strict fixture scoping to keep the two queries complementary, per the OQ5 rationale.
Recommendations
Section titled “Recommendations”A) callees (PRODUCT inv 2) — implementation design
Section titled “A) callees (PRODUCT inv 2) — implementation design”- New:
tools/ast-dataflow/queries/callees.ts - Edit:
tools/ast-dataflow/types.ts(args + row types),tools/ast-dataflow/index.ts(exports),tools/ast-dataflow/cli.ts(case + catalogue entry) - New:
tools/ast-dataflow/__tests__/callees.test.ts,tools/ast-dataflow/__tests__/fixtures/19-callees/
types.ts additions
Section titled “types.ts additions”/** How the call is spelled at the call site. */export type CalleeCallKind = 'call' | 'new' | 'super' | 'thisMethod';
export interface CalleesArgs { symbol: string; // '<file>:<name>' — same shape as callers limit?: number; // default 200 includeExternal?: boolean; // default false — see External callees below}
/** BaseResult position = the CALL SITE (caller side). */export interface CalleeResult extends BaseResult { enclosing: string; // findEnclosing(callExpr) — names the nested closure host calleeName: string; // rightmost identifier ('c' for a.b.c()), '<computed>' for obj[k](), '<anonymous>' for IIFEs callKind: CalleeCallKind; resolution: CallResolution; // reuse existing union (types.ts:3) — direct | aliased | destructured | computed-property | indirect (reexport unused here) importAlias?: string; // when resolution === 'aliased' /** Declared-side context. null file/line when unresolved or external. */ callee: { file: string | null; line: number | null }; external?: true; // declaration resolves outside the tsconfig corpus (node_modules / lib.d.ts)}confidence (inherited): 'exact' when the checker resolved a declaration (even for indirect resolution — the variable/parameter declaration is exactly resolved); 'indirect' only when no symbol resolves at all (matches TECH.md:261’s contract “resolution: ‘indirect’ when no symbol resolves” while keeping inv 15 semantics — resolution describes the call mechanism, confidence describes checker certainty).
Algorithm (callees.ts)
Section titled “Algorithm (callees.ts)”- Resolve the subject.
resolveSymbol(project, args.symbol, repoRoot)(resolve.ts:72). MapAstResolverErrorviabuildErrorResponseexactly as callers.ts:54-69. - Locate the body/bodies.
FunctionDeclaration/MethodDeclaration/FunctionExpression/ArrowFunction→getBody().VariableDeclaration→ unwrap initializer throughAsExpression/SatisfiesExpression/ParenthesizedExpression(same unwrap loop as resolve.ts:199-208) to an Arrow/FunctionExpression → its body.ClassDeclaration→ union of all method bodies + constructor + static blocks (each row’senclosingdisambiguates).- Anything else (interface, plain const, type alias) → structured error. Add ErrorKind
'not_callable'(additive enum change, allowed by inv 13) with hint “callees requires a function, method, arrow-function variable, or class; use references for non-callable symbols”. - Bodyless overload signatures / ambient declarations →
'not_callable'with an overload-specific hint.
- Enumerate call sites.
body.getDescendantsOfKind(SyntaxKind.CallExpression)+getDescendantsOfKind(SyntaxKind.NewExpression). Descendants inherently include calls inside nested closures — no special casing (the PRODUCT requirement “include — they’re in the body” is free). Sort merged list bygetStart()for stable output. Do NOT includeTaggedTemplateExpressionin V1 (not in inv 2’s wording; note as follow-up). - Resolve each callee (model on flow-trace.ts:409-421, but symbol-first rather than type-first):
Fallback whenconst expr = callExpr.getExpression(); // unwrap NonNullExpression/ParenthesizedExpression firstconst nameNode = rightmostName(expr); // Identifier | PropertyAccess name | ElementAccess | Super/This handlinglet sym = nameNode?.getSymbol();const aliased = sym?.getAliasedSymbol(); // import bindings resolve to the original via alias symbolconst decl = (aliased ?? sym)?.getDeclarations()[0];
symis undefined:expr.getType().getSymbol() ?? getAliasSymbol()(the flow-trace path) — catches some inferred-callable cases the name-symbol path misses. Still nothing →confidence: 'indirect',callee: { file: null, line: null },resolution: 'indirect'— reported, never dropped (inv 2). - Classify per callee-expression shape:
Identifier→ declaration kind decides: FunctionDeclaration/MethodDeclaration →direct; ImportSpecifier with alias node →aliased+importAlias(reuse/extract callers.tsclassifyResolution, generalised: move to resolve.ts so both queries share it);BindingElement(const { fn } = mod) →destructured;VariableDeclarationwhose initializer is an identifier/fn-ref (not an inline arrow) →indirect(PRODUCT’s “variable holding fn ref”), callee = the variable declaration site; inlineconst f = () => {}→direct(it IS the function);ParameterDeclaration(arrow params, callbacks) →indirect, callee = the parameter declaration site.PropertyAccessExpression(a.b(),a.b.c(), namespacens.fn()) → resolve the name node’s symbol (the checker resolves through inferred receiver types automatically — this answers “method calls on inferred types”:const s = makeSvc(); s.doThing()resolvesdoThingto the MethodDeclaration with no extra work). Chains only need the rightmost name; the receiver chain is irrelevant to resolution. Declaration = MethodDeclaration/PropertyDeclaration(arrow)/FunctionDeclaration →direct; function-typedPropertySignatureon an interface →directwith callee = the signature site (orexternalif in .d.ts outside corpus).super.m()→ expression of the PropertyAccess isSuperKeyword; resolves to the base-class method →resolution: 'direct',callKind: 'super'.this.m()→ resolves to the containing class’s member →direct,callKind: 'thisMethod'.ElementAccessExpression(obj[key]()): if the argument is a string/enum literal the checker may still resolve a symbol → classifycomputed-propertywith the resolved callee; unresolved (true dynamic) →computed-property+confidence: 'indirect'+calleeName: '<computed>', callee nulls. Reported, not dropped.NewExpression→ resolve the class symbol →callKind: 'new', resolution per the identifier rules above; callee line = the ClassDeclaration (or ConstructorDeclaration when present — prefer the constructor since that is what runs).- IIFE (
(() => {})()) → callee is the inline function itself:calleeName: '<anonymous>',direct, callee = the arrow’s own position.
- External callees (design decision, flag to owner): declarations in
node_modules/**or lib.d.ts(e.g.console.log,arr.map, supabase client methods) collide with inv 16 (“never emits paths outside the indexed corpus”). Default: exclude external rows (count them intotalEstimated? No — keep totalEstimated = matched rows; report anexternalCount: Ntop-level field instead so nothing is silently invisible). With--include-external: emit rows withexternal: true,callee: { file: null, line: null }, andcalleeName; never emit the node_modules path. This keeps the default output rename-sweep-sized while honouring “reported not dropped” via the count + opt-in flag. - Emit rows with the callers.ts cap idiom (
totalEstimated++thenif (rows.length >= limit) continue),file/line/columnfromcallExpr.getStart()viasf.getLineAndColumnAtPos,enclosing = findEnclosing(callExpr), callee-side viadecl.getSourceFile()+toRepoRelative+getLineAndColumnAtPos(decl.getStart()).
CLI wiring (cli.ts)
Section titled “CLI wiring (cli.ts)”Case block modelled on callers (cli.ts:376-391): require --symbol <file:name> else exit 2 with example; parseLimit; --include-external boolean; emitResponse(response, pretty). Catalogue entry after callers: { name: 'callees', args: ['--symbol <file:name>', '--include-external', '--limit N', '--pretty'], example: 'bun run ast-dataflow callees --symbol lib/procurement/procurement-queries.ts:getQuestions' }. Update the default-case valid-query list (cli.ts:837) and the catalogue notes line (cli.ts:247). index.ts: export { callees } from './queries/callees' + type exports.
Fixture corpus — __tests__/fixtures/19-callees/
Section titled “Fixture corpus — __tests__/fixtures/19-callees/”tsconfig.json (copy of 01-callers’), plus:
target.ts—export function subject()whose body contains: localhelper(); importedutil()(fromlib.ts); aliasedimport { util2 as u2 }thenu2()(fromlib2.ts);const fnRef = helper; fnRef(); a callback paramfunction subject(cb: () => void)withcb();handlers[name]()dynamic;arr.map((x) => helper2(x))(nested-closure inclusion + external.map);new Widget().service.ts—class Service { doThing() {} }+makeSvc(): Service; target calls bothsvcTyped.doThing()(annotated receiver) andmakeSvc().doThing()/const s = makeSvc(); s.doThing()(inferred receiver).chain.ts—api.client.get()property chain whereclientis a typed object property.class-fixture.ts—class Base { m() {} } class Sub extends Base { m() { super.m(); this.own(); } own() {} }.destructured.ts—const { fn } = mod; export function usesDestructured() { fn(); }.non-callable.ts—export const CONFIG = { a: 1 }(error-path fixture).
Test list (callees.test.ts — exact-count style per test-philosophy)
Section titled “Test list (callees.test.ts — exact-count style per test-philosophy)”- Direct local + imported calls: exact row set with caller
file/line/column,callee.file/line,resolution: 'direct',confidence: 'exact'(inv 23 hand-labelled equality). - Aliased import call →
resolution: 'aliased',importAlias: 'u2'(inv 25 analogue). - Method on annotated receiver AND on inferred receiver both resolve to
service.tsMethodDeclaration →direct. - Property chain
api.client.get()resolves rightmost name; callee =get’s declaration. fnRef()→resolution: 'indirect', callee = thefnRefVariableDeclaration site, present in output (inv 2 “reported not dropped”).- Callback param
cb()→indirect, callee = parameter declaration. handlers[name]()→computed-property,confidence: 'indirect',calleeName: '<computed>'.- Nested closure:
helper2row exists withenclosing: 'fn:subject'(arrow-in-callback resolution via findEnclosing). new Widget()→callKind: 'new'.super.m()→ callee = Base.m;this.own()→ callee = Sub.own; callKindssuper/thisMethod.- External default-off:
arr.mapabsent from rows, surfaced inexternalCount; withincludeExternal: truea row withexternal: true, callee.file: nullappears and no absolute/node_modules path exists anywhere in output (inv 16 assertion). - Class symbol as subject → rows from every method body.
- Non-callable symbol → structured error
not_callable, exit 0 via CLI (inv 29); unknown file →unknown_file. - Limit 2 on a ≥3-call body →
truncated: true,totalEstimatedcorrect (inv 14).
B) fixture-uses (PRODUCT inv 11) — implementation design
Section titled “B) fixture-uses (PRODUCT inv 11) — implementation design”- New:
tools/ast-dataflow/queries/fixture-uses.ts - Edit:
types.ts,index.ts,cli.ts - New:
tools/ast-dataflow/__tests__/fixture-uses.test.ts,tools/ast-dataflow/__tests__/fixtures/20-fixture-uses/
types.ts additions
Section titled “types.ts additions”export type FixtureUseKind = 'key' | 'value';export type FixtureFileType = 'json' | 'ts' | 'md-frontmatter';
export interface FixtureUsesArgs { needle: string; // exact string to find (column/table/magic literal) kinds?: FixtureUseKind[]; // filter; default both scope?: string; // optional comma-separated glob override of the default target set limit?: number; // default 200}
export interface FixtureUseResult extends BaseResult { confidence: 'indirect'; // inv 15: fixture grep is heuristic — always indirect kind: FixtureUseKind; // JSON/YAML/TS object-or-type KEY vs string VALUE (inv 11's split) fileType: FixtureFileType; /** Where in the structure: JSON path ('rows[2].project_id'), YAML path * ('baseline_values[0].key'), or TS enclosing via findEnclosing. */ context: string;}File discovery
Section titled “File discovery”Use tinyglobby (declared devDependency, package.json:197 — do NOT import fast-glob, which is only transitive). Default target set, rooted at repoRoot, evaluated fresh per invocation (no cache needed at this corpus size — dozens of files, well inside the 10 s heuristic budget of inv 19):
const DEFAULT_TARGETS = [ '__tests__/**/*.json', '__tests__/**/fixtures/**/*.{ts,tsx}', // "fixture by path" '__tests__/**/*-fixture.{ts,tsx}', // "fixture by convention" (see decision D1) 'e2e/fixtures/**/*.{ts,tsx,json,md}', 'scripts/tests/fixtures/**/*.{ts,tsx,json,md}', 'docs/ontology/*.md', // currently matches nothing — see decision D2 'supabase/types/database.types.ts',];ignore: ['**/node_modules/**']. Extension routing: .json → JSON mode; .ts/.tsx → TS mode; .md → frontmatter mode; anything else (e.g. e2e/fixtures/files/* binaries pulled in by a user --scope) silently skipped. --scope replaces the target list but keeps the same extension routing.
JSON mode — hand-rolled string-token lexer (no new dependency)
Section titled “JSON mode — hand-rolled string-token lexer (no new dependency)”jsonc-parser and stream-json are not available; TECH.md:270 explicitly blesses “hand-rolled”. A full parser is unnecessary — key-vs-value is a lexical property in JSON:
- Precompute a line-offset table (
indexOf('\n')sweep) for O(log n) offset→(line, column) conversion, 1-based (inv 13). - Scan for
"tokens; consume the string respecting\\escapes; decode viaJSON.parse(rawToken)so"project_id"matches needleproject_id; require exact equality with the needle (substring hits excluded, mirroring string-literal-uses semantics). - Classify: skip whitespace after the closing quote — next char
:→kind: 'key'; otherwise'value'. - Maintain a structural stack for
context: push on{/[(objects track last-seen key, arrays an index counter incremented on,at that depth) → emitrows[2].project_id-style paths. This is ~40 lines; if it proves fiddly,contextdegrades gracefully to''without breaking the row schema — but the stack version is recommended since tests will pin it. - Malformed JSON: do not throw — emit rows found up to the failure point; malformed fixture files are still greppable text. (Alternative: per-file
parse_errorerror row; rejected because inv 29’s error envelope is per-query, not per-file, and partial results are more useful mid-refactor.)
TS mode — ad-hoc ts-morph parses, NOT the main project
Section titled “TS mode — ad-hoc ts-morph parses, NOT the main project”Root tsconfig excludes scripts and supabase (tsconfig.json:66-74), so the passed-in project cannot see two of the six targets. Create one throwaway new Project({ skipAddingFilesFromTsConfig: true, compilerOptions: { allowJs: true, jsx: ts.JsxEmit.ReactJSX } }) and addSourceFileAtPath each discovered TS file. No type-checking is needed (everything is indirect confidence) — this is a pure parse, fast even for the large database.types.ts. Matching rules per file:
StringLiteral/NoSubstitutionTemplateLiteralwithgetLiteralValue() === needle:kind: 'value'— unless the literal is the name of aPropertyAssignment/PropertySignature/EnumMember(quoted keys), then'key'.PropertyAssignmentname Identifier=== needle(object-literal fixture rows:{ project_id: '...' }) →'key'.PropertySignaturename=== needle(thedatabase.types.tscase —project_id: stringinsideTables<...>Row/Insert/Update type literals) →'key'.context=findEnclosing(node)(resolve.ts:226) — works unchanged on ad-hoc-parsed files.- Do NOT reuse
string-literal-uses’ classifier: it intentionally drops object-literal values and non-call contexts (string-literal-uses.ts:116-119), which are precisely fixture-uses’ bread and butter. The two queries stay complementary (OQ5 rationale, PRODUCT.md:418-425).
Markdown frontmatter mode — yaml package (already a dep, already used in-repo)
Section titled “Markdown frontmatter mode — yaml package (already a dep, already used in-repo)”- Read the file; frontmatter = content between a leading
---\nand the next\n---(byte-offset tracked so positions map back to the md file). parseDocument(frontmatterText)fromyaml(v2.9, same import style as scripts/sync-intent-notes.ts:68), then walk with thevisitutility:Pairkey scalars withvalue === needle→'key'; scalar values (map values and sequence items)=== needle→'value'. Noderange[0]+ frontmatter start offset + the line-offset table → 1-based line/column.context= dotted YAML path from the visit ancestry (baseline_values[0].key).- Body text is never scanned (inv 11 says frontmatter; body search remains cocoindex/grep territory per PRODUCT Non-goals).
- No frontmatter or YAML parse failure → skip the file silently (same partial-tolerance rationale as JSON mode).
Response assembly
Section titled “Response assembly”Standard envelope (types.ts:52-65): sort rows file-then-line, apply kinds filter before the cap, cap at limit (default 200) with totalEstimated. Inv 14’s “distinct files first” truncation preference: iterate targets file-by-file and only thin multi-hit files after every file has contributed at least one row — implement as a two-pass emit (first hit per file, then remainder) — cheap here because the whole match set is in memory. Empty needle → parse_error structured error (mirror string-literal-uses.ts:130-139). Invalid kinds entry → parse_error listing valid kinds.
CLI wiring
Section titled “CLI wiring”bun run ast-dataflow fixture-uses --needle project_id [--kinds key,value] [--scope GLOB[,GLOB...]] [--limit N] [--pretty]Case block modelled on string-literal-uses (cli.ts:619-640): require --needle (exit 2 + example otherwise); split/validate --kinds against ['key','value'] with the references-style invalid-value error (cli.ts:427-432); catalogue entry + valid-query list + notes update; index.ts exports.
Fixture corpus — __tests__/fixtures/20-fixture-uses/ (repoRoot = fixture dir, replicating real layout)
Section titled “Fixture corpus — __tests__/fixtures/20-fixture-uses/ (repoRoot = fixture dir, replicating real layout)”tsconfig.json (unused by the query but keeps makeProject uniform)__tests__/fixtures/rows.json needle 'project_id' as key (×2, one nested in an array of rows) and as value (×1); decoy 'project_id_old' value; escaped "project_id" key__tests__/fixtures/typed-rows.ts object literal with project_id key + 'project_id' string value + template literal__tests__/unit/not-a-fixture.test.ts contains the needle — must NOT be scanned (convention gate)e2e/fixtures/seed-fixture.ts needle as object keye2e/fixtures/payload.json needle as valuee2e/fixtures/files/blob.bin skipped extension, no errorscripts/tests/fixtures/snapshot.json needle as keydocs/ontology/01-taxonomy.md frontmatter: needle as YAML key and as YAML value; needle also in the md BODY (must not match)supabase/types/database.types.ts hand-written miniature: Row/Insert type literals with project_id PropertySignature + a string-literal union containing the needleTest list (fixture-uses.test.ts)
Section titled “Test list (fixture-uses.test.ts)”- JSON key vs value separation: exact counts per kind for
rows.json; the value row and key rows carry correctkind(inv 11’s core split). - Exact-match discipline:
project_id_olddecoy absent; escapedekey matches (decode-then-compare). - Position fidelity: assert exact
line/column(1-based) for one known key and one known value (inv 13). contextpaths: nested JSON row reportsrows[1].project_id-style path; YAML row reportsbaseline_values[0].key-style path.kinds: ['key']filter returns only key rows; invalid kind string →parse_error.- Convention gate: needle in
__tests__/unit/not-a-fixture.test.tsproduces no row;-fixture.tsand/fixtures/files do. database.types.ts: PropertySignature match →kind: 'key',fileType: 'ts'; union string literal →'value'.- Frontmatter: key + value rows from
01-taxonomy.mdwithfileType: 'md-frontmatter'; body occurrence absent. - All target roots contribute (e2e + scripts/tests/fixtures rows present);
blob.bincauses no row and no error. - Every row:
confidence: 'indirect'(inv 15), repo-root-relative POSIX path, no absolute paths (inv 16). - Truncation: needle with >limit hits across ≥2 files →
truncated: true,totalEstimated, and both files represented in the capped rows (inv 14 spatial preference). - Empty needle →
parse_errorstructured error, CLI exit 0 (inv 29). - Malformed JSON fixture: rows before the syntax error still returned, no crash.
Decisions to put to the owner before implementation (spec ambiguities)
Section titled “Decisions to put to the owner before implementation (spec ambiguities)”- D1 — “fixture by convention” definition. Proposed:
/fixtures/path segment OR*-fixture.tsbasename (both attested in-repo:e2e/fixtures/change-reports-fixture.ts,test-data-fixture.ts). Anything looser (e.g. any file under__tests__/containing only data) is undecidable statically. - D2 —
docs/ontology/*.mdtarget is dead in-repo. The ontology docs live in the private docs-site (${KH_PRIVATE_DOCS_DIR}/src/content/docs/ontology/); scanning there violates inv 30 (worktree-scoped reads) and inv 16 (in-corpus paths). Proposed: keep the glob (harmless no-op, self-heals if the docs return), document the drift in PRODUCT.md, and rely on--scopefor ad-hoc frontmatter sweeps elsewhere in-repo. Rejected alternative: env-gated private-docs scan — needs a PRODUCT amendment first. - D3 —
database.types.tskind mapping. Proposed: PropertySignature names →key, string-literal union members (enum-ish values) →value; document in the row-type JSDoc since inv 11 phrases the split in JSON terms only. - (A-side) D4 — external callees. Proposed: excluded by default +
externalCountfield +--include-externalemittingcallee.file: nullrows; never emit node_modules paths (inv 16). Needs a one-line PRODUCT/TECH note since inv 2 says “every function/method called”. - (A-side) D5 — new ErrorKind
not_callable. Additive enum extension to types.ts:43-50; consistent with the flow-trace precedent of query-specific kinds (ORIGIN_NOT_RESOLVABLE).
Sequencing note
Section titled “Sequencing note”The two queries are independent (no shared new code beyond an optional extraction of classifyResolution from callers.ts into resolve.ts for callees) and can be built in parallel worktrees. Both should update ROADMAP.md’s DEFERRED table (lines 73-74) and the CLI catalogue notes on landing.