@cleya/contributor-react-navigation 0.0.0-stage → 1.0.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,137 @@
1
- # Temporary Holding Version
1
+ # @cleya/contributor-react-navigation
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Indexes a React Navigation app so you can ask **which screens exist, what renders
4
+ each one, and which screen can reach which other**.
5
+
6
+ It reads the source, not the running app: no instrumentation, no navigation
7
+ container, nothing to add to the project being indexed.
8
+
9
+ ## What it writes
10
+
11
+ ```
12
+ (Route)-[:SERVES]->(Screen)-[:RENDERS]->(Symbol)
13
+ (Screen)-[:NAVIGATES_TO]->(Route)
14
+ (Screen)-[:GATED_BY]->(Predicate)
15
+ ```
16
+
17
+ | Node | One per |
18
+ | --- | --- |
19
+ | `Route` | destination name — `"Settings"` |
20
+ | `Screen` | *registration* of a route — the same route registered in two navigators is two screens |
21
+ | `Predicate` | named mount condition — a guard, or a feature flag read in the component |
22
+ | `Symbol` | code symbol rendering the screen. Written by the code indexer, joined here. |
23
+
24
+ `Route` and `Screen` are separate on purpose: a route registered twice keeps two
25
+ positions, two guards and two components, which a single node would flatten.
26
+
27
+ ## Use
28
+
29
+ ```bash
30
+ bun add @cleya/contributor-react-navigation @cleya/region-navigation @cleya/region-trace
31
+ ```
32
+
33
+ ```ts
34
+ // cleya.config.ts
35
+ import { defineConfig, defineContributorGroup, defineProjectConfig } from "@cleya/config";
36
+ import { tsScipContributor } from "@cleya/contributor-ts-scip";
37
+ import { reactNavigationContributor } from "@cleya/contributor-react-navigation";
38
+ import { regionNavigation } from "@cleya/region-navigation";
39
+ import { regionTrace } from "@cleya/region-trace";
40
+
41
+ export default defineConfig({
42
+ projectSettings: { db: "./.cleya/graph.lbug" },
43
+ projects: [
44
+ defineProjectConfig({
45
+ id: "my-app",
46
+ scope: { root: "/abs/path/to/app", globs: ["src/**/*.ts", "src/**/*.tsx"] },
47
+ contributorGroups: [
48
+ defineContributorGroup({
49
+ region: regionTrace,
50
+ members: [tsScipContributor()],
51
+ }),
52
+ defineContributorGroup({
53
+ region: regionNavigation,
54
+ members: [reactNavigationContributor()],
55
+ }),
56
+ ],
57
+ }),
58
+ ],
59
+ });
60
+ ```
61
+
62
+ ```bash
63
+ cleya index
64
+ ```
65
+
66
+ ## Ask it things
67
+
68
+ ```cypher
69
+ -- every screen, and what renders it
70
+ MATCH (r:Route)-[:SERVES]->(s:Screen)
71
+ RETURN r.name AS route, s.graph AS navigator, s.component AS component
72
+ ORDER BY route
73
+
74
+ -- where can I get to from the settings screen, and by clicking what
75
+ MATCH (:Route {name: "Settings"})-[:SERVES]->(:Screen)-[n:NAVIGATES_TO]->(target:Route)
76
+ RETURN target.name, n.trigger, n.label, n.file, n.line
77
+
78
+ -- screens nothing navigates to: dead ends, or entry points
79
+ MATCH (r:Route)-[:SERVES]->(s:Screen)
80
+ WHERE NOT (:Screen)-[:NAVIGATES_TO]->(r)
81
+ RETURN r.name
82
+
83
+ -- what does this feature flag gate?
84
+ MATCH (s:Screen)-[:GATED_BY]->(p:Predicate {name: "canUseTravel"})
85
+ RETURN s.route
86
+ ```
87
+
88
+ ## Is the graph complete?
89
+
90
+ That is the question this plugin takes seriously, because the honest answer is
91
+ often "no". Every run prints a coverage report:
92
+
93
+ ```
94
+ [react-navigation] couverture des appels `navigate` : 1503/1711 (88 %)
95
+ 208 sans arête : 12+ hors registre · 61 avec motif d'URL · 135 opaques
96
+ cible lisible mais absente du registre — inscription probablement ratée :
97
+ src/pages/Search.tsx:88 ROUTES.SEARCH_ADVANCED
98
+ [react-navigation] 214 routes / 231 screens / 229 rendered / …
99
+ ```
100
+
101
+ The denominator matters more than the count. A plugin that finds nothing reports
102
+ zero edges, and zero looks exactly like "this app has no navigation" — so the
103
+ report always says how many navigation-shaped sites it *saw*, and gives the
104
+ address of the ones it could not use.
105
+
106
+ If a run comes back `partial`, read the diagnostics before reading the graph.
107
+
108
+ ## A project that does not look like the defaults
109
+
110
+ The core is the React Navigation API and needs no configuration. Anything the
111
+ project invented — its own navigation helpers, its own registry keys, screens
112
+ named by URL rather than by name — is a **dialect**, and it is configured:
113
+
114
+ ```ts
115
+ reactNavigationContributor({
116
+ // `goToOffer(id)` navigates; group 1 is the route name
117
+ helperCalls: [/goTo(\w+)\s*\(/g],
118
+ // the app navigates by URL, and only the linking config relates the two
119
+ routeAliasFiles: ["navigation/linkingConfig.ts"],
120
+ })
121
+ ```
122
+
123
+
124
+ ## Requirements
125
+
126
+ - Runs **after** a code contributor such as `@cleya/contributor-ts-scip`, in the same
127
+ project. Screens link to the symbols it declares; without them the plugin
128
+ refuses to run rather than write a graph with no components.
129
+ - `typescript` is a peer dependency: route names are resolved by the compiler,
130
+ using the project's own version.
131
+ - No `tsconfig.json` is not fatal — resolution falls back to enums and constant
132
+ paths, and literal route names are still found.
133
+
134
+ ## Further
135
+
136
+ - `CLAUDE.md` — architecture, invariants, module map. Lives alongside the
137
+ source, not in the published package.
package/dist/ast.d.ts ADDED
@@ -0,0 +1,64 @@
1
+ /**
2
+ * What sets a navigation off, as a reader of the screen would name it.
3
+ *
4
+ * Both fields are `""` when nothing carries the site — a route named at module
5
+ * level, outside any handler or tag. Empty is a fact, not a failure: it says the
6
+ * position is reachable by no interaction, which is what tells a spurious edge
7
+ * from a real one.
8
+ */
9
+ export interface SiteContext {
10
+ /** The trigger: JSX tag, handler, or declaration carrying the site. */
11
+ trigger: string;
12
+ /** The user-visible label, when the site sits inside a JSX tag. */
13
+ label: string;
14
+ }
15
+ /**
16
+ * The three syntactic questions a pattern cannot answer, over one parsed file.
17
+ *
18
+ * Every offset passed to these methods must be a position *inside* the file the
19
+ * index was built from; offsets from another file silently describe whatever
20
+ * node happens to sit there. Positions are byte offsets into the original
21
+ * source, not line/column, and not the blanked copy the patterns scan.
22
+ *
23
+ * The index holds its `SourceFile` for its whole lifetime — it is meant to live
24
+ * as long as the extraction of one file and be dropped, not cached across a run.
25
+ */
26
+ export interface AstIndex {
27
+ /** Is the position inside a type? Then it never runs. */
28
+ typeOnly(offset: number): boolean;
29
+ /** What carries this position, and under which label. */
30
+ context(offset: number, labelProps: string[]): SiteContext;
31
+ /**
32
+ * Identity of the literal carrying this position — its node's start.
33
+ *
34
+ * Two patterns often see the same string from two angles:
35
+ *
36
+ * navigateTo={{ screen: "SignupMethods" }}
37
+ * ^^^^^^^^^^ declarative-link pattern ^^^^^^ config-key pattern
38
+ *
39
+ * When the expression spans two lines, deduplicating by line number takes
40
+ * them for two navigations. The node is the same either way: it is the only
41
+ * identity that does not depend on how the code is laid out.
42
+ */
43
+ literalAt(offset: number): number;
44
+ }
45
+ /**
46
+ * Parse one file and answer positional questions about it.
47
+ *
48
+ * `path` is used only to pick the parser dialect — `.tsx` enables JSX, anything
49
+ * else does not — so a mislabelled path silently reads `<Tag/>` as a comparison.
50
+ * Parsing never throws: TypeScript recovers from syntax errors and yields a
51
+ * partial tree, so a broken file degrades to wrong answers rather than a crash.
52
+ *
53
+ * @example
54
+ * const src = `export type Props = { navigateTo: { screen: "VenueMap" } };`;
55
+ * astIndex("a.ts", src).typeOnly(src.indexOf(`"VenueMap"`)); // true — an annotation
56
+ *
57
+ * const run = `const go = () => ({ screen: "VenueMap" });`;
58
+ * astIndex("a.ts", run).typeOnly(run.indexOf(`"VenueMap"`)); // false — a value
59
+ *
60
+ * const jsx = `const C = () => <Touchable.Link wording="Voir" onPress={() => navigate("Home")} />;`;
61
+ * astIndex("a.tsx", jsx).context(jsx.indexOf(`"Home"`), ["wording"]);
62
+ * // { trigger: "Touchable.Link", label: "Voir" }
63
+ */
64
+ export declare const astIndex: (path: string, src: string) => AstIndex;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Blank out comments and string bodies **while preserving offsets**, so that
3
+ * walking braces is safe.
4
+ *
5
+ * The returned copy has exactly the length of the source: an index found on it
6
+ * designates the same character in the original. That is what lets us search
7
+ * for structure on the blanked text and read the value from the source.
8
+ *
9
+ * @example
10
+ * stripNoise('const a = "{";'); // 'const a = ;' — the brace no longer counts
11
+ * stripNoise("x // y"); // 'x ' — the comment becomes blanks
12
+ * stripNoise("x // y").length; // 6 — the length never changes
13
+ *
14
+ * A block comment is blanked the same way, delimiters included.
15
+ */
16
+ export declare const stripNoise: (src: string) => string;
17
+ /**
18
+ * Line number of an offset, **zero-based**.
19
+ *
20
+ * @example
21
+ * lineAt("a\nb\nc", 4); // 2 — not 3: the first line is line 0
22
+ */
23
+ export declare const lineAt: (src: string, index: number) => number;
24
+ /**
25
+ * Brace depth at this offset. Read it on the blanked copy: a brace inside a
26
+ * string would throw the count off.
27
+ *
28
+ * @example
29
+ * depthAt("a{b{c}d}e", 4); // 2
30
+ * depthAt("a{b{c}d}e", 8); // 0 — everything is closed again
31
+ */
32
+ export declare const depthAt: (blanked: string, index: number) => number;
33
+ /**
34
+ * An attribute's value, and *where it is*.
35
+ *
36
+ * `at` is the index of the expression within the attribute text: the
37
+ * type-based resolver designates a node by its range, not by its text — two
38
+ * attributes may carry the same expression and not name the same thing.
39
+ */
40
+ export interface Attr {
41
+ literal: string | null;
42
+ expr: string | null;
43
+ at: number | null;
44
+ }
45
+ /**
46
+ * Value of a **top-level** JSX attribute: `name="X"` → literal `X`,
47
+ * `name={Expr}` → expression `Expr`.
48
+ *
49
+ * Depth is everything:
50
+ *
51
+ * <Tab.Screen name="Search" options={{ tabBarIcon: () => <Icon name="images" /> }} />
52
+ *
53
+ * holds two `name=`. A regex search over the tag returns the last one — the
54
+ * icon's name — and registers one phantom route per tab. So only attributes at
55
+ * depth zero within the tag are accepted.
56
+ *
57
+ * @example
58
+ * const attrs = ` name="Search" options={{ tabBarIcon: () => <Icon name="images" /> }}`;
59
+ * topLevelAttr(attrs, stripNoise(attrs), "name").literal; // "Search", not "images"
60
+ * topLevelAttr(" name={SCREENS.HOME}", …, "name"); // { literal: null, expr: "SCREENS.HOME", at: 7 }
61
+ * topLevelAttr(" component={X}", …, "name"); // { literal: null, expr: null, at: null }
62
+ */
63
+ export declare const topLevelAttr: (attrsSrc: string, attrsBlanked: string, attr: string) => Attr;
64
+ /**
65
+ * Bounds of `const <symbol> = …`, anchored on the `=`.
66
+ *
67
+ * Anchoring on `=` is not a detail: a type annotation such as
68
+ * `const rootScreens: RouteConfig[] = [...]` would otherwise capture its own
69
+ * brackets.
70
+ *
71
+ * @example
72
+ * spanOf(stripNoise("const a = { x: 1 };"), "a"); // [10, 18]
73
+ * spanOf(stripNoise("const a = 1;"), "a"); // null — neither object nor array
74
+ * spanOf(stripNoise("const b = {};"), "a"); // null — no such symbol
75
+ */
76
+ export declare const spanOf: (blanked: string, symbol: string) => [number, number] | null;
77
+ /** One object entry found at depth 1, with its four bounds. */
78
+ export interface ObjectKey {
79
+ key: string;
80
+ at: number;
81
+ keyAt: number;
82
+ valueAt: number;
83
+ valueEnd: number;
84
+ }
85
+ /**
86
+ * An object's direct keys, literal and computed alike.
87
+ *
88
+ * `at` locates the entry — that is the line we report; `keyAt` locates the
89
+ * key *expression*, which is not the same thing once it is computed:
90
+ * `[SCREENS.X]` starts one character further along, and that is the bound the
91
+ * checker expects.
92
+ *
93
+ * @example
94
+ * const src = stripNoise("const r = { Home: A, [SCREENS.X]: B };");
95
+ * keysAtDepth1(src, src.indexOf("{")).map((k) => k.key); // ["Home", "SCREENS.X"]
96
+ */
97
+ export declare const keysAtDepth1: (blanked: string, objStart: number) => ObjectKey[];
@@ -0,0 +1,43 @@
1
+ /**
2
+ * What the contributor saw and could not use.
3
+ *
4
+ * An axis that never matches returns zero, and zero looks like "this project
5
+ * has none". That is the costliest mistake for whoever is discovering a
6
+ * codebase: the graph looks complete when it is amputated. The census gives the
7
+ * **denominator** — the sites shaped like a navigation that produced nothing —
8
+ * along with the means to go and look: file, line, expression.
9
+ */
10
+ export interface Census {
11
+ jsxTags: number;
12
+ jsxMissed: string[];
13
+ callsSeen: number;
14
+ callsResolved: number;
15
+ /** Target readable but unknown to the registry: a registration probably missed. */
16
+ unknown: string[];
17
+ /** No literal, but the type carries a URL pattern: recoverable by pattern join. */
18
+ patterned: string[];
19
+ patternedCount: number;
20
+ /** Opaque: irreducible, but it has to be counted. */
21
+ dynamic: string[];
22
+ }
23
+ /**
24
+ * An empty census, every counter at zero.
25
+ *
26
+ * @example
27
+ * const census = newCensus();
28
+ * census.callsSeen; // 0
29
+ * census.jsxMissed; // [] — samples accumulate through `sample`
30
+ */
31
+ export declare const newCensus: () => Census;
32
+ /**
33
+ * Record an example, up to a fixed cap.
34
+ *
35
+ * Silently ignores anything past the cap — the caller keeps the real magnitude
36
+ * in its own counter, so a sample list is an address book, not a measure.
37
+ *
38
+ * @example
39
+ * const into: string[] = [];
40
+ * for (let i = 0; i < 10; i++) sample(into, `line ${i}`);
41
+ * into.length; // 5 — capped, and the first five are the ones kept
42
+ */
43
+ export declare const sample: (into: string[], line: string) => void;
@@ -0,0 +1,113 @@
1
+ import type { SourceModel } from "@cleya/config";
2
+ import type { Domain } from "@cleya/domain";
3
+ import { type Census } from "./census";
4
+ import { type Counters, type Dialect } from "./dialect";
5
+ import type { GraphReader } from "./graph-reader";
6
+ import { type Registration } from "./registrations";
7
+ /** What `collect` needs. No connection, no Cypher. */
8
+ export interface CollectDeps {
9
+ source: SourceModel;
10
+ projectRoot: string;
11
+ graph: GraphReader;
12
+ /**
13
+ * First segment of every id built, per the recipes on
14
+ * `Domain.Region.Navigation`, so the model is ready to load as-is.
15
+ */
16
+ workspace: string;
17
+ }
18
+ /** One screen registration, resolved and ready to write. */
19
+ export interface ScreenRecord {
20
+ /** `"$workspace|$graph|$route"` */
21
+ id: string;
22
+ route: string;
23
+ graph: string;
24
+ file: string;
25
+ line: number;
26
+ shape: Registration["shape"];
27
+ guard: string;
28
+ hasOptions: boolean;
29
+ /** Component symbol id, or `""` when none could be resolved. */
30
+ component: string;
31
+ }
32
+ /** One navigation edge, already attributed, deduplicated and weighted. */
33
+ export interface NavEdge {
34
+ screen: string;
35
+ route: string;
36
+ kind: Domain.Region.Navigation.Kind;
37
+ via: Domain.Region.Navigation.Via;
38
+ file: string;
39
+ line: number;
40
+ trigger: string;
41
+ label: string;
42
+ own: boolean;
43
+ owners: number;
44
+ }
45
+ /**
46
+ * Everything the extraction learned but could not turn into an edge, plus the
47
+ * counts a run is judged by. Feeds the log and the diagnostics, never the graph.
48
+ */
49
+ export interface CollectReport {
50
+ counters: Counters;
51
+ census: Census;
52
+ duplicates: string[];
53
+ unlinked: string[];
54
+ mounts: string[];
55
+ ambiguous: string[];
56
+ multi: string[];
57
+ orphanCalls: number;
58
+ borrowed: number;
59
+ factoryFiles: number;
60
+ callSites: number;
61
+ rendered: number;
62
+ gates: number;
63
+ typeIndex: {
64
+ sourceFiles: number;
65
+ buildMs: number;
66
+ } | null;
67
+ adjacencySize: number;
68
+ }
69
+ /** The whole result of an extraction, before anything is written. */
70
+ export interface NavigationModel {
71
+ /** `id` is `"$workspace|$name"`. */
72
+ routes: Array<{
73
+ id: string;
74
+ name: string;
75
+ registrations: number;
76
+ }>;
77
+ screens: ScreenRecord[];
78
+ renders: Array<{
79
+ screen: string;
80
+ symbol: string;
81
+ }>;
82
+ /** `id` is `"$workspace|$kind|$name"`. */
83
+ predicates: Array<{
84
+ id: string;
85
+ name: string;
86
+ kind: string;
87
+ }>;
88
+ gates: Array<{
89
+ screen: string;
90
+ predicate: string;
91
+ }>;
92
+ edges: NavEdge[];
93
+ report: CollectReport;
94
+ }
95
+ /**
96
+ * Read the project and work out its navigation graph.
97
+ *
98
+ * Four passes: read every in-scope file, find the registrations, find the call
99
+ * sites, then attribute each call site to the screens whose component closure
100
+ * reaches it. Nothing is written — the result is a value, which is what makes
101
+ * every decision here assertable without a database.
102
+ *
103
+ * Degrades rather than fails when the project offers no usable TypeScript
104
+ * program: `createTypeIndex` returns `null` and resolution falls back to the
105
+ * name tables, so literal route names are still found.
106
+ *
107
+ * @example
108
+ * const model = await collect({ source, projectRoot, graph, workspace: "app" }, dialect);
109
+ * model.routes.map((r) => r.name); // ["Home", "Away"]
110
+ * model.edges.length; // navigations attributed to a screen
111
+ * model.report.census.callsSeen; // denominator: navigate() calls, edge or not
112
+ */
113
+ export declare const collect: (deps: CollectDeps, d: Dialect) => Promise<NavigationModel>;
@@ -0,0 +1,58 @@
1
+ import type { ReactNavigationDialect } from "./react-navigation.types";
2
+ /**
3
+ * Defaults.
4
+ *
5
+ * Empty where no universal convention exists — `helperCalls` and `predicates`.
6
+ * A wrong default is worse than a missing one: it manufactures a plausible
7
+ * graph, whereas an empty axis shows up in the match counts. The other axes
8
+ * have a default because React Navigation itself names them.
9
+ */
10
+ declare const DEFAULTS: {
11
+ helperCalls: RegExp[];
12
+ configKeys: string[];
13
+ labelProps: string[];
14
+ registryEntry: {
15
+ name: string;
16
+ component: string;
17
+ };
18
+ guardKey: string;
19
+ predicates: Array<{
20
+ kind: string;
21
+ pattern: RegExp;
22
+ }>;
23
+ exclude: string[];
24
+ routeAliasFiles: string[];
25
+ closureDepth: number;
26
+ graphLabel: (path: string) => string;
27
+ };
28
+ /** A dialect with every axis resolved, ready for the extraction to read. */
29
+ export type Dialect = typeof DEFAULTS & {
30
+ /** Axes set explicitly: never matching one is an error, not a choice. */
31
+ configured: Set<string>;
32
+ };
33
+ /**
34
+ * Resolve a partial dialect into a complete one, validating its patterns.
35
+ *
36
+ * Throws when a supplied pattern is not global or captures nothing — at
37
+ * configuration load time, not midway through an indexing run.
38
+ *
39
+ * @example
40
+ * resolveDialect({}).configured.size; // 0 — everything defaulted
41
+ * resolveDialect({ closureDepth: 3 }).closureDepth; // 3
42
+ * resolveDialect({ helperCalls: [/nav/g] }); // throws — no capture group
43
+ */
44
+ export declare const resolveDialect: (dialect?: ReactNavigationDialect) => Dialect;
45
+ /** Match count per axis: a pattern that never fires has to be visible. */
46
+ export type Counters = Record<string, number>;
47
+ /**
48
+ * Increment an axis counter.
49
+ *
50
+ * @example
51
+ * const counters: Counters = {};
52
+ * bump(counters, "types"); // { types: 1 }
53
+ * bump(counters, "types", 5); // { types: 6 } — an absent axis starts at 0
54
+ */
55
+ export declare const bump: (counters: Counters, axis: string, n?: number) => void;
56
+ /** Recognises a file mounting a navigator: `createXNavigator(` or `<`. */
57
+ export declare const FACTORY: RegExp;
58
+ export {};
@@ -0,0 +1,55 @@
1
+ import type { Domain } from "@cleya/domain";
2
+ /** A symbol reachable from a navigator file, as a component candidate. */
3
+ export type SymbolRow = {
4
+ file: string;
5
+ name: string;
6
+ symbolId: string;
7
+ };
8
+ /** A top-level declaration of a page file, with its kind. */
9
+ export type DeclaredRow = SymbolRow & {
10
+ type: string;
11
+ };
12
+ /**
13
+ * Everything the extraction reads from the code graph, and nothing more.
14
+ *
15
+ * Four reads, all scoped to one workspace. They are a port rather than a
16
+ * client because `collect` is where the contributor's judgement lives, and
17
+ * judgement that issues Cypher can only be tested against a database. A fake
18
+ * here is four array literals.
19
+ *
20
+ * This is not a general graph API: each method exists because one specific step
21
+ * of the extraction needs exactly that shape.
22
+ */
23
+ export interface GraphReader {
24
+ /**
25
+ * `USES` adjacency for the workspace, source symbol id -> target symbol ids.
26
+ *
27
+ * Loaded whole rather than walked lazily: the closure visits it repeatedly
28
+ * and per-query round trips dominated the run.
29
+ */
30
+ adjacency(): Promise<Map<string, string[]>>;
31
+ /**
32
+ * Symbols declared by these files, plus the symbols those declarations use.
33
+ *
34
+ * Both halves matter: a navigator may declare its screens itself, or import
35
+ * them. Matching by name over this set is what avoids re-implementing module
36
+ * resolution.
37
+ */
38
+ componentCandidates(files: string[]): Promise<SymbolRow[]>;
39
+ /** Every indexed file path of the workspace, for module-specifier resolution. */
40
+ filePaths(): Promise<string[]>;
41
+ /** Top-level declarations of these files, to find a page's main symbol. */
42
+ declaredSymbols(files: string[]): Promise<DeclaredRow[]>;
43
+ }
44
+ /**
45
+ * The production reader: one Cypher query per method over the trace region,
46
+ * scoped to the files `workspace` indexed. A symbol used across workspaces
47
+ * (an `external|…` one) still appears as a `USES` target.
48
+ *
49
+ * @example
50
+ * const reader = cypherReader(ctx.db, ctx.workspace);
51
+ * (await reader.adjacency()).size; // symbols with at least one USES edge
52
+ * await reader.componentCandidates(["Navigator.tsx"]); // [{ file, name, symbolId }, …]
53
+ * await reader.componentCandidates([]); // [] — no query issued
54
+ */
55
+ export declare const cypherReader: (db: Domain.Db.ReadClient, workspace: string) => GraphReader;
@@ -0,0 +1,93 @@
1
+ import type { Domain } from "@cleya/domain";
2
+ import { type Census } from "./census";
3
+ import { type Counters, type Dialect } from "./dialect";
4
+ import { type Classify, type Resolve } from "./route-names";
5
+ /** One navigation call site: who navigates, to which route, under which label. */
6
+ export interface CallSite {
7
+ route: string;
8
+ file: string;
9
+ line: number;
10
+ kind: Domain.Region.Navigation.Kind;
11
+ via: Domain.Region.Navigation.Via;
12
+ /** Trigger id: link component, config symbol, or enclosing function. */
13
+ trigger: string;
14
+ /** User-visible label, when the trigger carries one. */
15
+ label: string;
16
+ }
17
+ /**
18
+ * Call patterns: the core first, then the dialect.
19
+ *
20
+ * The core is the React Navigation API, identical everywhere. The last two axes
21
+ * — config keys and project helpers — depend entirely on the project's
22
+ * conventions: they carry 94 % of the edges on one repository and zero
23
+ * elsewhere without configuration.
24
+ *
25
+ * @example
26
+ * callPatterns(resolveDialect({})).length; // core patterns only
27
+ * callPatterns(resolveDialect({ helperCalls: [/go\((\w+)\)/g] }));
28
+ * // …plus one entry whose axis is "helperCalls", so a silent helper is visible
29
+ */
30
+ export declare const callPatterns: (d: Dialect) => Array<{
31
+ re: RegExp;
32
+ kind: Domain.Region.Navigation.Kind;
33
+ via: Domain.Region.Navigation.Via;
34
+ axis: string;
35
+ ref?: true;
36
+ }>;
37
+ /**
38
+ * Past this many variants, a union of patterns no longer discriminates.
39
+ *
40
+ * A conditional navigation aims at two or three targets; a union carrying
41
+ * dozens is the project's sum type, arrived at through an `as Route`.
42
+ */
43
+ export declare const PATTERN_LIMIT = 8;
44
+ /**
45
+ * Targets named somewhere other than right after the call.
46
+ *
47
+ * The `callPatterns` patterns require the literal glued to `navigate(`. Two
48
+ * forms escape them, and both are real navigations:
49
+ *
50
+ * reset({ routes: [{ name: "Bookings" }] }) the stack rebuilt in one go
51
+ * navigate(cond ? "A" : "B") the target chosen at runtime
52
+ *
53
+ * The argument list is delimited by balancing parens, then two precise patterns
54
+ * are applied inside it. Precise, and not "every string in the region": a
55
+ * region overruns as soon as a paren sits inside a string, and the sweep then
56
+ * brings back neighbouring `case "Profile":`. The graph filters unknown names,
57
+ * but it cannot tell a real route name quoted in the wrong place from a target.
58
+ *
59
+ * @example
60
+ * spreadTargets(src, blanked, new Set(["Home", "Away"]));
61
+ * // navigate(x ? "Home" : "Away") → two entries, one per branch
62
+ */
63
+ export declare const spreadTargets: (src: string, blanked: string, routeNames: Set<string>) => Array<{
64
+ route: string;
65
+ index: number;
66
+ kind: Domain.Region.Navigation.Kind;
67
+ }>;
68
+ /**
69
+ * Every navigation call site in one file, plus what the census learned from it.
70
+ *
71
+ * Mutates `counters` and `census` rather than returning them: both aggregate
72
+ * across the whole run, and threading them back through every file would make
73
+ * the caller reassemble what it already owns.
74
+ *
75
+ * @example
76
+ * const sites = callSitesIn(src, stripNoise(src), "a.tsx", routeNames, dialect,
77
+ * resolve, counters, census, classify);
78
+ * sites.map((s) => s.route); // ["Home"] — `navigate("Home")`
79
+ * // A literal inside a type annotation yields nothing: it never runs.
80
+ * // A ternary yields two sites, one per branch.
81
+ */
82
+ export declare const callSitesIn: (src: string, blanked: string, path: string, routeNames: Set<string>, d: Dialect, resolve: Resolve, counters: Counters, census: Census, classify: Classify) => CallSite[];
83
+ /**
84
+ * Symbol ids are `{workspace}|{scope}|{path}|{name}` — the file is segment 2.
85
+ *
86
+ * Returns `""` rather than throwing on a malformed id: the ids come from the
87
+ * code graph, and one bad row must not abort a run.
88
+ *
89
+ * @example
90
+ * fileOfSymbolId("ws|internal|src/Home.tsx|Home"); // "src/Home.tsx"
91
+ * fileOfSymbolId("not-an-id"); // "" — no segment 2
92
+ */
93
+ export declare const fileOfSymbolId: (id: string) => string;