FlowSpec Codebase Annotator
Index a codebase and extract FlowSpec-compatible elements (datapoints, components, transforms, tables, screens, data flows) from source files. Produces a persistent index with per-file timestamps so only new or modified files are re-processed on subsequent runs.
Invocation
/flowspec:annotate [path]
path (optional) — directory to index. Defaults to the current working directory.
Behaviour
- Load or create the index file at
<project-root>/.flowspec/codebase-index.json.
- Discover source files — walk the target directory for indexable files (see File Selection below).
- Diff against the index — for each discovered file, compare its filesystem
mtime to the lastIndexed timestamp stored in the index. Skip files whose mtime is older than or equal to lastIndexed.
- Analyse changed files — read each new/modified file and extract FlowSpec elements (see Extraction Rules below).
- Update the index — merge new extractions into the index, remove entries for deleted files, and rebuild the aggregated spec.
- Write the index — save
.flowspec/codebase-index.json.
- Report — summarise what changed: files scanned, files skipped (unchanged), elements found, and the path to the index file.
File Selection
Include
Scan files matching these patterns:
**/*.ts **/*.tsx **/*.js **/*.jsx
**/*.svelte **/*.vue **/*.astro
**/*.py **/*.go **/*.rs **/*.java **/*.kt
**/*.sql **/*.prisma **/*.graphql **/*.gql
**/*.json (only package.json, tsconfig.json, schema files)
**/*.yaml **/*.yml (only schema/config files)
Exclude
Always skip:
node_modules/ .git/ dist/ build/
.svelte-kit/ .next/ .nuxt/ .output/
__pycache__/ target/ vendor/
*.min.js *.min.css *.map
*.lock *.log *.png *.jpg *.svg *.ico *.woff*
.env* *.pem *.key
Also skip any paths listed in .gitignore (if present).
Index File Format
The index is stored at .flowspec/codebase-index.json:
{
"version": "1.0.0",
"createdAt": "2026-02-12T10:00:00.000Z",
"lastRunAt": "2026-02-12T12:30:00.000Z",
"projectRoot": "/absolute/path/to/project",
"config": {
"include": ["**/*.ts", "**/*.svelte"],
"exclude": ["node_modules/**"]
},
"files": {
"src/routes/login/+page.svelte": {
"lastIndexed": "2026-02-12T12:30:00.000Z",
"fileModified": "2026-02-12T11:45:00.000Z",
"sizeBytes": 2340,
"elements": {
"dataPoints": [ /* ... */ ],
"components": [ /* ... */ ],
"transforms": [ /* ... */ ],
"tables": [ /* ... */ ],
"images": [ /* ... */ ],
"dataFlow": [ /* ... */ ],
"screens": [ /* ... */ ]
}
}
// ... one entry per indexed file
},
"spec": {
// Aggregated FlowSpec JSON (v1.2.0) — ready for import or MCP consumption
// See "Aggregated Spec" section below
}
}
Per-File elements Object
Each file entry contains the FlowSpec elements extracted from that file. IDs are deterministic and scoped to the file to avoid collisions:
ID format: idx:<relative-path>:<element-type>:<ordinal> (or idx:<workspace>:<relative-path>:<element-type>:<ordinal> in monorepos — see Edge Cases below)
Example: idx:src/routes/login/+page.svelte:dp:0
This ensures IDs are stable across re-indexes (same file + same element = same ID) and globally unique across the project.
Aggregated Spec
The top-level spec key contains a merged FlowSpec JSON document (v1.2.0 format) assembled from all file entries. This is the primary output — it can be:
- Imported directly into FlowSpec via MCP:
flowspec_create_project with the spec
- Appended to an existing project via individual
flowspec_create_node / flowspec_create_edge calls
- Read by any tool that understands FlowSpec JSON
{
"version": "1.2.0",
"metadata": {
"projectName": "<directory-name> (indexed)",
"exportedAt": "2026-02-12T12:30:00.000Z",
"nodeCount": 42,
"edgeCount": 31,
"sourceFiles": 15,
"indexVersion": "1.0.0"
},
"dataPoints": [],
"components": [],
"transforms": [],
"tables": [],
"images": [],
"dataFlow": [],
"screens": []
}
Extraction Rules
When reading a source file, identify and categorise the following elements into FlowSpec node types. Use the file's purpose, exports, and structure to determine what belongs where.
DataPoints
A DataPoint represents a discrete piece of data that flows through the application. Extract these from:
| Source Pattern |
DataPoint Properties |
Form input fields (<input>, <select>, bind:value) |
source: "captured", type from input type |
State variables ($state, useState, ref(), let x = ... in reactive context) |
source: "captured" if user-set, "inferred" if computed |
Props / component parameters ($props(), export let, function params on components) |
source: "captured", type from TypeScript annotation |
| API response fields (destructured fetch results, return types) |
source: "inferred", sourceDefinition: "API: <endpoint>" |
| Database column values (query results, ORM model fields) |
source: "inferred", sourceDefinition: "DB: <table>.<column>" |
Environment variables (process.env.*, $env/*) |
source: "inferred", sourceDefinition: "env" |
| URL parameters / search params |
source: "captured", sourceDefinition: "URL param" |
Security — .env files:
Index variable names only — never index values, secrets, or credentials. The .env* exclusion pattern above already prevents reading .env files. Exception: .env.example and .env.template may be read for variable names only (they should not contain real secrets). Recommend adding .flowspec/ to .gitignore so the index (which contains file paths and element names) is not committed.
DataPoint JSON:
{
"id": "idx:src/routes/login/+page.svelte:dp:0",
"label": "User Email",
"type": "string",
"source": "captured",
"sourceDefinition": "Text input on login form",
"constraints": ["required", "email format"],
"locations": [
{ "component": "idx:src/routes/login/+page.svelte:comp:0", "role": "input" }
]
}
Type mapping:
string — text inputs, string variables, enum-like values
number — numeric inputs, counters, IDs, amounts
boolean — checkboxes, toggles, flags, boolean state
object — complex objects, nested data, JSON blobs
array — lists, collections, multi-select values
Constraints — include when determinable:
"required" — non-optional, validated as present
"unique" — must be unique (DB constraints, unique validation)
"email format", "URL format", "phone format" — format validation
"min: N", "max: N", "minLength: N", "maxLength: N" — range/length
"enum: [values]" — restricted set of values
"readonly" — not user-modifiable (computed, derived)
Constraint detection algorithm — extract constraints from these sources (check all that apply to the file's language):
- TypeScript types:
? property → omit "required"; union literal types (e.g., "a" | "b") → "enum: [a, b]"; Readonly<T> or as const → "readonly"
- Zod schemas:
.min(N) → "min: N"; .max(N) → "max: N"; .email() → "email format"; .url() → "URL format"; .optional() → omit "required"; .regex(pattern) → "pattern: <pattern>"
- HTML5 attributes:
required → "required"; minlength="N" → "minLength: N"; maxlength="N" → "maxLength: N"; type="email" → "email format"; type="url" → "URL format"; pattern="..." → "pattern: ..."; min="N" / max="N" → "min: N" / "max: N"
- Framework validators: React Hook Form
rules: { required, minLength, ... }, Svelte use:validate actions, Formik/Yup schema chains — map their constraint names to the same FlowSpec constraint strings above
- Database schema:
NOT NULL → "required"; UNIQUE → "unique"; CHECK(...) → extract the condition; Prisma @unique / @default / Drizzle .notNull() / .unique() — same mapping
Merge rule: Union all constraints found across sources. If two sources specify conflicting numeric ranges (e.g., HTML min="0" and Zod .min(1)), prefer the stricter (larger min, smaller max) value.
locations format: Each entry in the locations array has { "component": "<Component node ID>", "role": "input" | "output" }. "input" means the component captures this DataPoint (it appears in the component's captures list); "output" means the component displays it (it appears in displays). Populated during Step 5.9, not during per-file extraction.
Components
A Component represents a UI element or page that displays and/or captures data. Extract from:
| Source Pattern |
Component |
Svelte/Vue/React component files (.svelte, .vue, .tsx) |
One component per file |
Exported page components (+page.svelte, page.tsx, index.vue) |
Component with wireframeRef = route path |
Layout components (+layout.svelte, layout.tsx) |
Component wrapping child components |
| Reusable UI components (form groups, cards, modals, tables) |
Component with displays/captures from props |
Extracting children — 6-step algorithm:
- Parse imports: Collect all component imports — named (
import { Button } from), default (import Button from), aliased (import { Button as Btn }), and namespace (import * as Icons from). Identify which resolve to component files (.svelte, .vue, .tsx, .jsx) vs utility modules.
- Search template for usage: For each imported component, search the template/JSX for actual usage. Match by tag name:
<Button, <Btn, <Icons.Home, {#each ... as} wrappers, conditional renders ({#if}, ? ... :). A component that is imported but never appears in the template is not a child.
- Handle dynamic imports:
React.lazy(() => import('./Heavy')), Svelte {#await import('./Heavy') then mod}, Vue defineAsyncComponent(() => import('./Heavy')) — resolve the path, treat the result as a child if it appears in the template.
- Handle slot-based children: When a component defines
<slot> (Svelte), {children} (React), or <slot> (Vue), the children are provided by the parent, not the slot-defining component. Do not list slot-injected content as children of the component defining the slot.
- Order by first template appearance: Sort children by the line/character offset of their first usage in the template. This preserves visual order.
- Build
children array: Each child entry is the child component's index ID (idx:<path>:comp:<ordinal>). During per-file extraction, if the child component hasn't been indexed yet, use a placeholder ID based on the import path — resolve to the real ID during aggregation (Step 5).
Extracting layout: Inspect the component's outermost template wrapper:
| Source Pattern |
Layout |
display: flex / flex / Tailwind flex class |
type: "flex", check flex-direction or flex-col/flex-row for direction |
display: grid / Tailwind grid class |
type: "grid", check grid-template-areas for named areas |
| Sidebar + main pattern (two children, one narrow) |
type: "sidebar" |
Tab/tabbed interface (<Tabs>, role="tablist") |
type: "tabs" |
| Vertical stack of sections with no explicit layout |
type: "stack", direction: "column" |
| No detectable structure |
type: "free" |
Extracting sizeHint: Count displays + captures for fieldCount. Count children array length for childCount. Apply category thresholds.
Component JSON:
{
"id": "idx:src/routes/login/+page.svelte:comp:0",
"label": "Login Page",
"wireframeRef": "/login",
"displays": ["idx:src/routes/login/+page.svelte:dp:2"],
"captures": ["idx:src/routes/login/+page.svelte:dp:0", "idx:src/routes/login/+page.svelte:dp:1"],
"children": ["idx:src/lib/components/LoginForm.svelte:comp:0", "idx:src/lib/components/OAuthButtons.svelte:comp:0"],
"layout": {
"type": "flex",
"direction": "column",
"areas": ["header", "main", "footer"]
},
"sizeHint": {
"fieldCount": 3,
"childCount": 2,
"category": "medium"
}
}
displays — IDs of DataPoints this component renders (read-only display)
captures — IDs of DataPoints this component captures from user input
wireframeRef — the route path or meaningful reference string
children — IDs of child Components this component imports and renders in its template. Establishes the nesting hierarchy (page → section → form → input).
layout — how child elements are arranged:
type — "flex", "grid", "stack", "sidebar", "tabs", or "free" (inferred from CSS classes, Tailwind utilities, or wrapper elements)
direction — "row" or "column" (for flex/stack)
areas — named layout regions if detectable (e.g., grid-template-areas, slot names, semantic sections like header/main/footer)
sizeHint — data to help the canvas estimate node dimensions:
fieldCount — total displays + captures (how much content the component shows)
childCount — number of children
category — "small" (0-2 fields, 0 children), "medium" (3-6 fields or 1-3 children), "large" (7+ fields or 4+ children)
Transforms
A Transform represents business logic, data processing, or validation. Extract from:
| Source Pattern |
Transform |
Validation functions (Zod schemas, validate(), form validators) |
type: "validation" |
Computed / derived values ($derived, useMemo, computed properties) |
type: "formula" |
| Data transformation functions (mappers, formatters, parsers) |
type: "formula" |
API route handlers (+server.ts, api/*.ts, controllers) |
type: "workflow" |
| Authentication logic (login, session checks, guards) |
type: "workflow" |
| Database queries with logic (joins, aggregations, filters) |
type: "formula" |
| Multi-step processes (checkout flows, wizards, state machines) |
type: "workflow", logic type "steps" |
Transform JSON:
{
"id": "idx:src/lib/auth.ts:tx:0",
"type": "validation",
"description": "Validate login credentials against database",
"inputs": ["idx:src/routes/login/+page.svelte:dp:0", "idx:src/routes/login/+page.svelte:dp:1"],
"outputs": ["idx:src/lib/auth.ts:dp:0"],
"logic": {
"type": "steps",
"content": "1. Validate email format\n2. Query user by email\n3. Compare password hash\n4. Generate session token"
}
}
Tables
A Table represents a persistent data source. Extract from:
| Source Pattern |
Table |
Database schema definitions (SQL CREATE TABLE, Prisma model, Drizzle schema) |
sourceType: "database" |
| API endpoint definitions (REST routes, GraphQL types) |
sourceType: "api", endpoint = URL |
| Static data files (JSON configs, CSV, seed data) |
sourceType: "file" |
| In-memory stores (global stores, context providers with initial data) |
sourceType: "manual" |
Table JSON:
{
"id": "idx:src/lib/server/db/schema.sql:tbl:0",
"label": "users",
"sourceType": "database",
"columns": [
{ "name": "id", "type": "string" },
{ "name": "email", "type": "string" },
{ "name": "password_hash", "type": "string" },
{ "name": "created_at", "type": "string" }
],
"endpoint": "postgres://neon/users"
}
Images
An Image represents a static visual asset referenced in the codebase. Extract from:
| Source Pattern |
Image |
Static imports (import logo from './logo.png') |
url = import path |
HTML <img> / <Image> tags with src attribute |
url = src value |
CSS/Tailwind background images (background-image: url(...), bg-[url(...)]) |
url = extracted URL |
Public asset references (/images/hero.png, /static/logo.svg) |
url = public path |
Svelte {@html} or framework image components (<Image>, <Picture>, next/image) |
url = resolved src |
Image JSON:
{
"id": "idx:src/lib/components/Header.svelte:img:0",
"label": "Company Logo",
"url": "/images/logo.svg",
"width": 200,
"height": 40,
"opacity": 1.0
}
width / height — extract from explicit attributes (width="200", w-[200px]), CSS, or inline styles. Set to null if not determinable.
opacity — extract from CSS opacity property or Tailwind opacity-* class. Default to 1.0.
- Images are visual-only nodes — they create no edges. They provide context for canvas rendering.
- To import images into FlowSpec, use
flowspec_upload_image MCP tool (not flowspec_create_node).
Screens
A Screen represents a page or view in the application. Extract from route definitions:
| Source Pattern |
Screen |
SvelteKit routes (src/routes/**/+page.svelte) |
One screen per route |
Next.js pages (app/**/page.tsx, pages/**/*.tsx) |
One screen per route |
Vue routes (src/views/*.vue + router config) |
One screen per route |
| Generic route configs (React Router, etc.) |
One screen per route entry |
Building regions from source: Each screen gets regions by analysing the page component's template structure:
- Identify top-level sections in the page template (header, nav, main content, sidebar, footer, modals).
- Each distinct section becomes a region. The region's
label comes from the semantic element, component name, or aria-label.
- Assign
position and size percentages by estimating from the page layout (use the component's layout data). For example, in a column layout: header gets {x:0, y:0, size:{width:100, height:10}}, main gets {x:0, y:10, size:{width:100, height:80}}, footer gets {x:0, y:90, size:{width:100, height:10}}.
- Populate
elements by collecting all DataPoints rendered or captured within that section, in template order. Set order to preserve sequence.
- Link
componentNodeId to the Component that owns the region.
Screen JSON:
{
"id": "idx:screens:login",
"name": "Login Page",
"layout": {
"type": "flex",
"direction": "column",
"areas": ["header", "main"]
},
"regions": [
{
"id": "idx:screens:login:region:0",
"label": "Login Form",
"position": { "x": 25, "y": 20 },
"size": { "width": 50, "height": 60 },
"elements": [
{ "nodeId": "idx:src/routes/login/+page.svelte:dp:0", "nodeLabel": "User Email", "nodeType": "datapoint", "order": 0 },
{ "nodeId": "idx:src/routes/login/+page.svelte:dp:1", "nodeLabel": "Password", "nodeType": "datapoint", "order": 1 }
],
"componentNodeId": "idx:src/routes/login/+page.svelte:comp:0"
}
]
}
layout — page-level layout structure (same schema as Component layout), inferred from the route's top-level template
regions[].elements[].order — the order the element appears in the source template (maps to position_order in the database). Preserves the visual sequence so the canvas can render fields in the same order as the original UI.
regions[].position — percentage (0-100) relative to the screen, estimating where the region sits. Infer from layout structure: a header region gets y: 0, a sidebar gets x: 0, a main content area gets centred values.
regions[].size — percentage (0-100) of the screen this region occupies. Infer from layout: a full-width header might be { width: 100, height: 10 }, a sidebar { width: 25, height: 90 }, a card form { width: 50, height: 60 }.
DataFlow (Edges)
Edges represent how data moves between elements. Extract from:
| Pattern |
Edge |
| Component renders a DataPoint (displays) |
from: datapoint, to: component, edgeType: "flows-to" |
| Component captures a DataPoint (captures) |
from: component, to: datapoint, edgeType: "flows-to" |
| Transform reads a DataPoint (input) |
from: datapoint, to: transform, edgeType: "transforms" |
| Transform produces a DataPoint (output) |
from: transform, to: datapoint, edgeType: "flows-to" |
| DataPoint derived from another |
from: source, to: derived, edgeType: "derives-from" |
| Validation checks a DataPoint |
from: transform, to: datapoint, edgeType: "validates" |
| Component reads from Table (API/DB fetch) |
from: table, to: component, edgeType: "flows-to" |
| Transform writes to Table (API/DB mutation) |
from: transform, to: table, edgeType: "flows-to" |
| Parent component renders child component (import + template use) |
from: parent, to: child, edgeType: "contains" |
| Cross-file imports (component imports util, page loads data) |
Connect by matching referenced IDs |
Edge JSON:
{
"from": "idx:src/routes/login/+page.svelte:dp:0",
"to": "idx:src/lib/auth.ts:tx:0",
"edgeType": "transforms",
"label": "login credential"
}
Edge type selection:
"flows-to" — default data movement (A provides data to B)
"derives-from" — B is computed from A (derived/computed values)
"transforms" — data passes through a transform/processing step
"validates" — a validation checks or constrains data
"contains" — containment relationship: screen contains component (auto-generated for screen regions), or parent component contains child component (auto-generated from children arrays). Use from: parent, to: child.
Step-by-Step Procedure
Step 1: Discover or Load Index
# Check for existing index
cat .flowspec/codebase-index.json 2>/dev/null
If the file exists, parse it. If not, initialise a new index structure:
{
"version": "1.0.0",
"createdAt": "<now>",
"lastRunAt": "<now>",
"projectRoot": "<cwd>",
"config": { "include": [], "exclude": [] },
"files": {},
"spec": null
}
Step 2: Discover Source Files
Use find or glob to list all indexable source files. Record each file's mtime.
Step 3: Diff
For each discovered file:
- If not in the index → mark as new (needs indexing)
- If in the index but
mtime > lastIndexed → mark as modified (needs re-indexing)
- If in the index and
mtime <= lastIndexed → skip
- If in the index but not on disk → remove from index
Step 4: Analyse Changed Files
For each new/modified file:
- Read the file contents
- Identify the file's role (UI component, API route, utility, schema, etc.)
- Extract elements following the Extraction Rules above
- Assign deterministic IDs using the
idx:<path>:<type>:<ordinal> format
- Store the
elements object in the file's index entry
- Set
lastIndexed to the current timestamp
- Set
fileModified to the file's mtime
ID stability and ordinal assignment:
- The
ordinal in idx:<path>:<type>:<ordinal> is the order of appearance in the source file (0-indexed). The first DataPoint found in a file is :dp:0, the second is :dp:1, etc.
- Stability guarantee: If a file is re-indexed and the elements appear in the same order, they receive the same IDs. Edges referencing those IDs remain valid across re-indexes.
- Limitation: If elements are reordered, added, or removed, ordinals shift for all subsequent elements of that type. An element that was
:dp:2 may become :dp:1 after a deletion.
- Stale edge handling: During Step 5.8 (edge validation), any edge referencing an ID that no longer exists is removed. Before removal, attempt label-based recovery: search for a node with the same
label in the same file. If found, log the ID change ("dp:2 → dp:1 (label: User Email)") in the report. Do not auto-fix — report only, so the user can review.
- Guidance: Prefer
label + file path for display and human communication. Use IDs only for edge references and internal lookups.
Step 5: Rebuild Aggregated Spec
Merge all per-file elements into a single FlowSpec v1.2.0 spec:
Collect all dataPoints from all files into spec.dataPoints
Collect all components from all files into spec.components
Collect all transforms from all files into spec.transforms
Collect all tables from all files into spec.tables
Collect all images from all files into spec.images
Collect all dataFlow edges from all files into spec.dataFlow
Collect all screens from all files into spec.screens
Cross-file edge resolution — 5-phase algorithm:
Phase A: Build the import graph. For every indexed file, parse its import statements. Resolve each import path to a relative file path in the index (handle $lib/, @/, ~/ aliases, index.ts barrel files, and * as namespace imports). Build a map: { importingFile → [{ importedFile, importedIdentifiers[] }] }.
Phase B: Match imported identifiers to indexed elements. For each imported identifier (function name, component name, variable name), search the target file's extracted elements for a node whose label matches (case-insensitive). Record matches as { importingFileElement, importedFileElement, relationship }.
Phase C: Create edges based on element type × usage pattern.
| Imported element type |
Usage in importing file |
Edge |
| Component |
Rendered in template (<Comp>) |
from: parent-comp, to: child-comp, edgeType: "contains" |
| Transform (function) |
Called with DataPoint args |
from: datapoint, to: transform, edgeType: "transforms" |
| DataPoint (exported var) |
Read in template or logic |
from: datapoint, to: component, edgeType: "flows-to" |
| Table (store/API client) |
Queried or mutated |
from: table, to: component/transform, edgeType: "flows-to" |
Phase D: Framework-specific patterns. Check for these cross-file patterns and add appropriate edges:
- SvelteKit:
+page.server.ts load() return → +page.svelte $props().data — connect load's output DataPoints to page component's displays
- React/Next.js:
getServerSideProps / loader return → page component props — same pattern
- Vue:
provide() in parent → inject() in child — connect the provided DataPoint to the consuming component
- Shared stores: Svelte stores (
writable), React context, Zustand/Pinia — connect the store (Table node) to all components that subscribe
Phase E: Deduplication. Before adding any cross-file edge, check if an edge with the same from, to, and edgeType already exists. Skip duplicates.
Validate edges: Remove any edge where from or to references an ID that no longer exists in the merged spec. Before removal, attempt label-based recovery (see ID stability section above).
Populate locations: For each Component in spec.components, iterate its displays array — for each DataPoint ID, add { "component": "<comp-id>", "role": "output" } to that DataPoint's locations. Then iterate its captures array — for each DataPoint ID, add { "component": "<comp-id>", "role": "input" } to that DataPoint's locations. Deduplicate entries with the same component + role.
Update spec.metadata with counts.
Step 6: Write Index File
mkdir -p .flowspec
# Write the complete index
Write the full index JSON to .flowspec/codebase-index.json with 2-space indentation.
Step 7: Report
Print a summary:
FlowSpec Codebase Index — complete
Files scanned: 15 (8 new, 4 updated, 3 skipped, 2 removed)
DataPoints: 42
Components: 12
Transforms: 8
Tables: 3
Images: 7
Screens: 5
Edges: 31
Index: .flowspec/codebase-index.json
Spec: .flowspec/codebase-index.json → spec (v1.2.0)
To import into FlowSpec:
flowspec_create_project({ name: "<project> (indexed)" })
# Then use the spec.dataPoints, spec.components, etc. with flowspec_create_node
Incremental Re-runs
On subsequent invocations, the skill:
- Loads the existing index
- Only processes files with
mtime > lastIndexed
- Preserves elements from unchanged files
- Rebuilds the aggregated spec from all files (changed + unchanged)
- Reports only what changed
This makes re-indexing fast — a 500-file project where 3 files changed only reads those 3 files.
Edge Cases
Binary / Non-text Files
Skip silently. The file selection patterns already exclude most binary types.
Very Large Files
Index fully — every variable, every function, every data flow. Large files often contain the most important application logic. If a file is too large to read in one pass, read it in chunks and merge the extracted elements. Never skip or summarise.
Generated Files
Skip files that contain @generated, // auto-generated, or similar markers in the first 5 lines.
Monorepo / Multi-package
If a workspaces key is present in package.json, index each workspace separately and prefix IDs with the workspace name: idx:<workspace>:<path>:<type>:<ordinal>.
Empty Projects
If no indexable files are found, create the index with an empty files object and a null spec. Report: "No indexable source files found."
Using the Index with FlowSpec MCP
Create a New Project from Index
flowspec_create_project({ name: "My App (indexed)" })
# Returns projectId
# Create nodes — flowspec_create_node accepts: datapoint, component, transform, table, actor
for each dataPoint in spec.dataPoints:
flowspec_create_node({ projectId, type: "datapoint", label: dataPoint.label, data: { ... } })
for each component in spec.components:
flowspec_create_node({ projectId, type: "component", label: component.label, data: { ... } })
for each transform in spec.transforms:
flowspec_create_node({ projectId, type: "transform", label: transform.description, data: { ... } })
for each table in spec.tables:
flowspec_create_node({ projectId, type: "table", label: table.label, data: { ... } })
for each actor in spec.actors:
flowspec_create_node({ projectId, type: "actor", label: actor.label, data: { actorType: actor.actorType, description: actor.description } })
# Create screens — use flowspec_create_screen (NOT flowspec_create_node)
for each screen in spec.screens:
flowspec_create_screen({ projectId, name: screen.name, layout: screen.layout })
# Returns screenId
for each region in screen.regions:
flowspec_add_region({ projectId, screenId, label: region.label, position: region.position, size: region.size, elements: region.elements })
# Upload images — use flowspec_upload_image (NOT flowspec_create_node)
for each image in spec.images:
flowspec_upload_image({ projectId, label: image.label, url: image.url, width: image.width, height: image.height })
# Create edges
for each edge in spec.dataFlow:
flowspec_create_edge({ projectId, source: edge.from, target: edge.to, edgeType: edge.edgeType })
# Auto-layout after import
flowspec_auto_layout({ projectId })
Important: flowspec_create_node accepts datapoint, component, transform, table, and actor as the type parameter. Screens and images have their own dedicated MCP tools.
Append to Existing Project
flowspec_search_nodes({ projectId, query: "..." }) # Check what already exists
# Only create nodes/edges that don't already exist
Analyse After Import
flowspec_analyse_project({ projectId })
# Reports orphan nodes, missing connections, duplicates
Configuration
Optional .flowspec/index-config.json to override defaults:
{
"include": ["src/**/*.ts", "src/**/*.svelte"],
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts", "**/*.d.ts"],
"maxFileSize": 100000,
"skipGenerated": true
}
If this file exists, use it. Otherwise, use the defaults from File Selection above.
Converted and distributed by TomeVault — claim your Tome and manage your conversions.
1---2name: jktfe-flowspec-claude-code-plugin-annotate3description: FlowSpec Codebase Annotator4---56# FlowSpec Codebase Annotator78Index a codebase and extract FlowSpec-compatible elements (datapoints, components, transforms, tables, screens, data flows) from source files. Produces a persistent index with per-file timestamps so only new or modified files are re-processed on subsequent runs.910## Invocation1112```13/flowspec:annotate [path]14```1516- `path` (optional) — directory to index. Defaults to the current working directory.1718## Behaviour19201. **Load or create the index file** at `<project-root>/.flowspec/codebase-index.json`.212. **Discover source files** — walk the target directory for indexable files (see File Selection below).223. **Diff against the index** — for each discovered file, compare its filesystem `mtime` to the `lastIndexed` timestamp stored in the index. Skip files whose `mtime` is older than or equal to `lastIndexed`.234. **Analyse changed files** — read each new/modified file and extract FlowSpec elements (see Extraction Rules below).245. **Update the index** — merge new extractions into the index, remove entries for deleted files, and rebuild the aggregated spec.256. **Write the index** — save `.flowspec/codebase-index.json`.267. **Report** — summarise what changed: files scanned, files skipped (unchanged), elements found, and the path to the index file.2728## File Selection2930### Include3132Scan files matching these patterns:3334```35**/*.ts **/*.tsx **/*.js **/*.jsx36**/*.svelte **/*.vue **/*.astro37**/*.py **/*.go **/*.rs **/*.java **/*.kt38**/*.sql **/*.prisma **/*.graphql **/*.gql39**/*.json (only package.json, tsconfig.json, schema files)40**/*.yaml **/*.yml (only schema/config files)41```4243### Exclude4445Always skip:4647```48node_modules/ .git/ dist/ build/49.svelte-kit/ .next/ .nuxt/ .output/50__pycache__/ target/ vendor/51*.min.js *.min.css *.map52*.lock *.log *.png *.jpg *.svg *.ico *.woff*53.env* *.pem *.key54```5556Also skip any paths listed in `.gitignore` (if present).5758## Index File Format5960The index is stored at `.flowspec/codebase-index.json`:6162```jsonc63{64 "version": "1.0.0",65 "createdAt": "2026-02-12T10:00:00.000Z",66 "lastRunAt": "2026-02-12T12:30:00.000Z",67 "projectRoot": "/absolute/path/to/project",68 "config": {69 "include": ["**/*.ts", "**/*.svelte"],70 "exclude": ["node_modules/**"]71 },72 "files": {73 "src/routes/login/+page.svelte": {74 "lastIndexed": "2026-02-12T12:30:00.000Z",75 "fileModified": "2026-02-12T11:45:00.000Z",76 "sizeBytes": 2340,77 "elements": {78 "dataPoints": [ /* ... */ ],79 "components": [ /* ... */ ],80 "transforms": [ /* ... */ ],81 "tables": [ /* ... */ ],82 "images": [ /* ... */ ],83 "dataFlow": [ /* ... */ ],84 "screens": [ /* ... */ ]85 }86 }87 // ... one entry per indexed file88 },89 "spec": {90 // Aggregated FlowSpec JSON (v1.2.0) — ready for import or MCP consumption91 // See "Aggregated Spec" section below92 }93}94```9596### Per-File `elements` Object9798Each file entry contains the FlowSpec elements extracted from that file. IDs are deterministic and scoped to the file to avoid collisions:99100**ID format:** `idx:<relative-path>:<element-type>:<ordinal>` (or `idx:<workspace>:<relative-path>:<element-type>:<ordinal>` in monorepos — see Edge Cases below)101Example: `idx:src/routes/login/+page.svelte:dp:0`102103This ensures IDs are stable across re-indexes (same file + same element = same ID) and globally unique across the project.104105## Aggregated Spec106107The top-level `spec` key contains a merged FlowSpec JSON document (v1.2.0 format) assembled from all file entries. This is the primary output — it can be:108109- Imported directly into FlowSpec via MCP: `flowspec_create_project` with the spec110- Appended to an existing project via individual `flowspec_create_node` / `flowspec_create_edge` calls111- Read by any tool that understands FlowSpec JSON112113```json114{115 "version": "1.2.0",116 "metadata": {117 "projectName": "<directory-name> (indexed)",118 "exportedAt": "2026-02-12T12:30:00.000Z",119 "nodeCount": 42,120 "edgeCount": 31,121 "sourceFiles": 15,122 "indexVersion": "1.0.0"123 },124 "dataPoints": [],125 "components": [],126 "transforms": [],127 "tables": [],128 "images": [],129 "dataFlow": [],130 "screens": []131}132```133134## Extraction Rules135136When reading a source file, identify and categorise the following elements into FlowSpec node types. Use the file's purpose, exports, and structure to determine what belongs where.137138### DataPoints139140A DataPoint represents a discrete piece of data that flows through the application. Extract these from:141142| Source Pattern | DataPoint Properties |143|---|---|144| Form input fields (`<input>`, `<select>`, `bind:value`) | `source: "captured"`, type from input type |145| State variables (`$state`, `useState`, `ref()`, `let x = ...` in reactive context) | `source: "captured"` if user-set, `"inferred"` if computed |146| Props / component parameters (`$props()`, `export let`, function params on components) | `source: "captured"`, type from TypeScript annotation |147| API response fields (destructured fetch results, return types) | `source: "inferred"`, `sourceDefinition: "API: <endpoint>"` |148| Database column values (query results, ORM model fields) | `source: "inferred"`, `sourceDefinition: "DB: <table>.<column>"` |149| Environment variables (`process.env.*`, `$env/*`) | `source: "inferred"`, `sourceDefinition: "env"` |150| URL parameters / search params | `source: "captured"`, `sourceDefinition: "URL param"` |151152> **Security — `.env` files:**153> Index variable **names** only — never index values, secrets, or credentials. The `.env*` exclusion pattern above already prevents reading `.env` files. Exception: `.env.example` and `.env.template` may be read for variable names only (they should not contain real secrets). Recommend adding `.flowspec/` to `.gitignore` so the index (which contains file paths and element names) is not committed.154155**DataPoint JSON:**156```json157{158 "id": "idx:src/routes/login/+page.svelte:dp:0",159 "label": "User Email",160 "type": "string",161 "source": "captured",162 "sourceDefinition": "Text input on login form",163 "constraints": ["required", "email format"],164 "locations": [165 { "component": "idx:src/routes/login/+page.svelte:comp:0", "role": "input" }166 ]167}168```169170**Type mapping:**171- `string` — text inputs, string variables, enum-like values172- `number` — numeric inputs, counters, IDs, amounts173- `boolean` — checkboxes, toggles, flags, boolean state174- `object` — complex objects, nested data, JSON blobs175- `array` — lists, collections, multi-select values176177**Constraints** — include when determinable:178- `"required"` — non-optional, validated as present179- `"unique"` — must be unique (DB constraints, unique validation)180- `"email format"`, `"URL format"`, `"phone format"` — format validation181- `"min: N"`, `"max: N"`, `"minLength: N"`, `"maxLength: N"` — range/length182- `"enum: [values]"` — restricted set of values183- `"readonly"` — not user-modifiable (computed, derived)184185**Constraint detection algorithm** — extract constraints from these sources (check all that apply to the file's language):1861871. **TypeScript types:** `?` property → omit `"required"`; union literal types (e.g., `"a" | "b"`) → `"enum: [a, b]"`; `Readonly<T>` or `as const` → `"readonly"`1882. **Zod schemas:** `.min(N)` → `"min: N"`; `.max(N)` → `"max: N"`; `.email()` → `"email format"`; `.url()` → `"URL format"`; `.optional()` → omit `"required"`; `.regex(pattern)` → `"pattern: <pattern>"`1893. **HTML5 attributes:** `required` → `"required"`; `minlength="N"` → `"minLength: N"`; `maxlength="N"` → `"maxLength: N"`; `type="email"` → `"email format"`; `type="url"` → `"URL format"`; `pattern="..."` → `"pattern: ..."`; `min="N"` / `max="N"` → `"min: N"` / `"max: N"`1904. **Framework validators:** React Hook Form `rules: { required, minLength, ... }`, Svelte `use:validate` actions, Formik/Yup schema chains — map their constraint names to the same FlowSpec constraint strings above1915. **Database schema:** `NOT NULL` → `"required"`; `UNIQUE` → `"unique"`; `CHECK(...)` → extract the condition; Prisma `@unique` / `@default` / Drizzle `.notNull()` / `.unique()` — same mapping192193**Merge rule:** Union all constraints found across sources. If two sources specify conflicting numeric ranges (e.g., HTML `min="0"` and Zod `.min(1)`), prefer the stricter (larger min, smaller max) value.194195**`locations` format:** Each entry in the `locations` array has `{ "component": "<Component node ID>", "role": "input" | "output" }`. `"input"` means the component captures this DataPoint (it appears in the component's `captures` list); `"output"` means the component displays it (it appears in `displays`). Populated during Step 5.9, not during per-file extraction.196197### Components198199A Component represents a UI element or page that displays and/or captures data. Extract from:200201| Source Pattern | Component |202|---|---|203| Svelte/Vue/React component files (`.svelte`, `.vue`, `.tsx`) | One component per file |204| Exported page components (`+page.svelte`, `page.tsx`, `index.vue`) | Component with `wireframeRef` = route path |205| Layout components (`+layout.svelte`, `layout.tsx`) | Component wrapping child components |206| Reusable UI components (form groups, cards, modals, tables) | Component with displays/captures from props |207208**Extracting children** — 6-step algorithm:2092101. **Parse imports:** Collect all component imports — named (`import { Button } from`), default (`import Button from`), aliased (`import { Button as Btn }`), and namespace (`import * as Icons from`). Identify which resolve to component files (`.svelte`, `.vue`, `.tsx`, `.jsx`) vs utility modules.2112. **Search template for usage:** For each imported component, search the template/JSX for actual usage. Match by tag name: `<Button`, `<Btn`, `<Icons.Home`, `{#each ... as}` wrappers, conditional renders (`{#if}`, `? ... :`). A component that is imported but never appears in the template is **not** a child.2123. **Handle dynamic imports:** `React.lazy(() => import('./Heavy'))`, Svelte `{#await import('./Heavy') then mod}`, Vue `defineAsyncComponent(() => import('./Heavy'))` — resolve the path, treat the result as a child if it appears in the template.2134. **Handle slot-based children:** When a component defines `<slot>` (Svelte), `{children}` (React), or `<slot>` (Vue), the children are provided by the **parent**, not the slot-defining component. Do not list slot-injected content as children of the component defining the slot.2145. **Order by first template appearance:** Sort children by the line/character offset of their first usage in the template. This preserves visual order.2156. **Build `children` array:** Each child entry is the child component's index ID (`idx:<path>:comp:<ordinal>`). During per-file extraction, if the child component hasn't been indexed yet, use a placeholder ID based on the import path — resolve to the real ID during aggregation (Step 5).216217**Extracting layout:** Inspect the component's outermost template wrapper:218| Source Pattern | Layout |219|---|---|220| `display: flex` / `flex` / Tailwind `flex` class | `type: "flex"`, check `flex-direction` or `flex-col`/`flex-row` for direction |221| `display: grid` / Tailwind `grid` class | `type: "grid"`, check `grid-template-areas` for named areas |222| Sidebar + main pattern (two children, one narrow) | `type: "sidebar"` |223| Tab/tabbed interface (`<Tabs>`, role="tablist") | `type: "tabs"` |224| Vertical stack of sections with no explicit layout | `type: "stack"`, `direction: "column"` |225| No detectable structure | `type: "free"` |226227**Extracting sizeHint:** Count displays + captures for `fieldCount`. Count `children` array length for `childCount`. Apply category thresholds.228229**Component JSON:**230```json231{232 "id": "idx:src/routes/login/+page.svelte:comp:0",233 "label": "Login Page",234 "wireframeRef": "/login",235 "displays": ["idx:src/routes/login/+page.svelte:dp:2"],236 "captures": ["idx:src/routes/login/+page.svelte:dp:0", "idx:src/routes/login/+page.svelte:dp:1"],237 "children": ["idx:src/lib/components/LoginForm.svelte:comp:0", "idx:src/lib/components/OAuthButtons.svelte:comp:0"],238 "layout": {239 "type": "flex",240 "direction": "column",241 "areas": ["header", "main", "footer"]242 },243 "sizeHint": {244 "fieldCount": 3,245 "childCount": 2,246 "category": "medium"247 }248}249```250251- `displays` — IDs of DataPoints this component renders (read-only display)252- `captures` — IDs of DataPoints this component captures from user input253- `wireframeRef` — the route path or meaningful reference string254- `children` — IDs of child Components this component imports and renders in its template. Establishes the nesting hierarchy (page → section → form → input).255- `layout` — how child elements are arranged:256 - `type` — `"flex"`, `"grid"`, `"stack"`, `"sidebar"`, `"tabs"`, or `"free"` (inferred from CSS classes, Tailwind utilities, or wrapper elements)257 - `direction` — `"row"` or `"column"` (for flex/stack)258 - `areas` — named layout regions if detectable (e.g., grid-template-areas, slot names, semantic sections like header/main/footer)259- `sizeHint` — data to help the canvas estimate node dimensions:260 - `fieldCount` — total displays + captures (how much content the component shows)261 - `childCount` — number of children262 - `category` — `"small"` (0-2 fields, 0 children), `"medium"` (3-6 fields or 1-3 children), `"large"` (7+ fields or 4+ children)263264### Transforms265266A Transform represents business logic, data processing, or validation. Extract from:267268| Source Pattern | Transform |269|---|---|270| Validation functions (Zod schemas, `validate()`, form validators) | `type: "validation"` |271| Computed / derived values (`$derived`, `useMemo`, computed properties) | `type: "formula"` |272| Data transformation functions (mappers, formatters, parsers) | `type: "formula"` |273| API route handlers (`+server.ts`, `api/*.ts`, controllers) | `type: "workflow"` |274| Authentication logic (login, session checks, guards) | `type: "workflow"` |275| Database queries with logic (joins, aggregations, filters) | `type: "formula"` |276| Multi-step processes (checkout flows, wizards, state machines) | `type: "workflow"`, logic type `"steps"` |277278**Transform JSON:**279```json280{281 "id": "idx:src/lib/auth.ts:tx:0",282 "type": "validation",283 "description": "Validate login credentials against database",284 "inputs": ["idx:src/routes/login/+page.svelte:dp:0", "idx:src/routes/login/+page.svelte:dp:1"],285 "outputs": ["idx:src/lib/auth.ts:dp:0"],286 "logic": {287 "type": "steps",288 "content": "1. Validate email format\n2. Query user by email\n3. Compare password hash\n4. Generate session token"289 }290}291```292293### Tables294295A Table represents a persistent data source. Extract from:296297| Source Pattern | Table |298|---|---|299| Database schema definitions (SQL `CREATE TABLE`, Prisma `model`, Drizzle schema) | `sourceType: "database"` |300| API endpoint definitions (REST routes, GraphQL types) | `sourceType: "api"`, `endpoint` = URL |301| Static data files (JSON configs, CSV, seed data) | `sourceType: "file"` |302| In-memory stores (global stores, context providers with initial data) | `sourceType: "manual"` |303304**Table JSON:**305```json306{307 "id": "idx:src/lib/server/db/schema.sql:tbl:0",308 "label": "users",309 "sourceType": "database",310 "columns": [311 { "name": "id", "type": "string" },312 { "name": "email", "type": "string" },313 { "name": "password_hash", "type": "string" },314 { "name": "created_at", "type": "string" }315 ],316 "endpoint": "postgres://neon/users"317}318```319320### Images321322An Image represents a static visual asset referenced in the codebase. Extract from:323324| Source Pattern | Image |325|---|---|326| Static imports (`import logo from './logo.png'`) | `url` = import path |327| HTML `<img>` / `<Image>` tags with `src` attribute | `url` = src value |328| CSS/Tailwind background images (`background-image: url(...)`, `bg-[url(...)]`) | `url` = extracted URL |329| Public asset references (`/images/hero.png`, `/static/logo.svg`) | `url` = public path |330| Svelte `{@html}` or framework image components (`<Image>`, `<Picture>`, `next/image`) | `url` = resolved src |331332**Image JSON:**333```json334{335 "id": "idx:src/lib/components/Header.svelte:img:0",336 "label": "Company Logo",337 "url": "/images/logo.svg",338 "width": 200,339 "height": 40,340 "opacity": 1.0341}342```343344- `width` / `height` — extract from explicit attributes (`width="200"`, `w-[200px]`), CSS, or inline styles. Set to `null` if not determinable.345- `opacity` — extract from CSS `opacity` property or Tailwind `opacity-*` class. Default to `1.0`.346- Images are **visual-only** nodes — they create no edges. They provide context for canvas rendering.347- To import images into FlowSpec, use `flowspec_upload_image` MCP tool (not `flowspec_create_node`).348349### Screens350351A Screen represents a page or view in the application. Extract from route definitions:352353| Source Pattern | Screen |354|---|---|355| SvelteKit routes (`src/routes/**/+page.svelte`) | One screen per route |356| Next.js pages (`app/**/page.tsx`, `pages/**/*.tsx`) | One screen per route |357| Vue routes (`src/views/*.vue` + router config) | One screen per route |358| Generic route configs (React Router, etc.) | One screen per route entry |359360**Building regions from source:** Each screen gets regions by analysing the page component's template structure:3611. Identify top-level sections in the page template (header, nav, main content, sidebar, footer, modals).3622. Each distinct section becomes a region. The region's `label` comes from the semantic element, component name, or aria-label.3633. Assign `position` and `size` percentages by estimating from the page layout (use the component's `layout` data). For example, in a column layout: header gets `{x:0, y:0, size:{width:100, height:10}}`, main gets `{x:0, y:10, size:{width:100, height:80}}`, footer gets `{x:0, y:90, size:{width:100, height:10}}`.3644. Populate `elements` by collecting all DataPoints rendered or captured within that section, in template order. Set `order` to preserve sequence.3655. Link `componentNodeId` to the Component that owns the region.366367**Screen JSON:**368```json369{370 "id": "idx:screens:login",371 "name": "Login Page",372 "layout": {373 "type": "flex",374 "direction": "column",375 "areas": ["header", "main"]376 },377 "regions": [378 {379 "id": "idx:screens:login:region:0",380 "label": "Login Form",381 "position": { "x": 25, "y": 20 },382 "size": { "width": 50, "height": 60 },383 "elements": [384 { "nodeId": "idx:src/routes/login/+page.svelte:dp:0", "nodeLabel": "User Email", "nodeType": "datapoint", "order": 0 },385 { "nodeId": "idx:src/routes/login/+page.svelte:dp:1", "nodeLabel": "Password", "nodeType": "datapoint", "order": 1 }386 ],387 "componentNodeId": "idx:src/routes/login/+page.svelte:comp:0"388 }389 ]390}391```392393- `layout` — page-level layout structure (same schema as Component `layout`), inferred from the route's top-level template394- `regions[].elements[].order` — the order the element appears in the source template (maps to `position_order` in the database). Preserves the visual sequence so the canvas can render fields in the same order as the original UI.395- `regions[].position` — percentage (0-100) relative to the screen, estimating where the region sits. Infer from layout structure: a header region gets `y: 0`, a sidebar gets `x: 0`, a main content area gets centred values.396- `regions[].size` — percentage (0-100) of the screen this region occupies. Infer from layout: a full-width header might be `{ width: 100, height: 10 }`, a sidebar `{ width: 25, height: 90 }`, a card form `{ width: 50, height: 60 }`.397398### DataFlow (Edges)399400Edges represent how data moves between elements. Extract from:401402| Pattern | Edge |403|---|---|404| Component renders a DataPoint (displays) | `from: datapoint, to: component, edgeType: "flows-to"` |405| Component captures a DataPoint (captures) | `from: component, to: datapoint, edgeType: "flows-to"` |406| Transform reads a DataPoint (input) | `from: datapoint, to: transform, edgeType: "transforms"` |407| Transform produces a DataPoint (output) | `from: transform, to: datapoint, edgeType: "flows-to"` |408| DataPoint derived from another | `from: source, to: derived, edgeType: "derives-from"` |409| Validation checks a DataPoint | `from: transform, to: datapoint, edgeType: "validates"` |410| Component reads from Table (API/DB fetch) | `from: table, to: component, edgeType: "flows-to"` |411| Transform writes to Table (API/DB mutation) | `from: transform, to: table, edgeType: "flows-to"` |412| Parent component renders child component (import + template use) | `from: parent, to: child, edgeType: "contains"` |413| Cross-file imports (component imports util, page loads data) | Connect by matching referenced IDs |414415**Edge JSON:**416```json417{418 "from": "idx:src/routes/login/+page.svelte:dp:0",419 "to": "idx:src/lib/auth.ts:tx:0",420 "edgeType": "transforms",421 "label": "login credential"422}423```424425**Edge type selection:**426- `"flows-to"` — default data movement (A provides data to B)427- `"derives-from"` — B is computed from A (derived/computed values)428- `"transforms"` — data passes through a transform/processing step429- `"validates"` — a validation checks or constrains data430- `"contains"` — containment relationship: screen contains component (auto-generated for screen regions), or parent component contains child component (auto-generated from `children` arrays). Use `from: parent, to: child`.431432## Step-by-Step Procedure433434### Step 1: Discover or Load Index435436```bash437# Check for existing index438cat .flowspec/codebase-index.json 2>/dev/null439```440441If the file exists, parse it. If not, initialise a new index structure:442443```json444{445 "version": "1.0.0",446 "createdAt": "<now>",447 "lastRunAt": "<now>",448 "projectRoot": "<cwd>",449 "config": { "include": [], "exclude": [] },450 "files": {},451 "spec": null452}453```454455### Step 2: Discover Source Files456457Use `find` or glob to list all indexable source files. Record each file's `mtime`.458459### Step 3: Diff460461For each discovered file:462- If not in the index → mark as **new** (needs indexing)463- If in the index but `mtime > lastIndexed` → mark as **modified** (needs re-indexing)464- If in the index and `mtime <= lastIndexed` → **skip**465- If in the index but not on disk → **remove** from index466467### Step 4: Analyse Changed Files468469For each new/modified file:4701. Read the file contents4712. Identify the file's role (UI component, API route, utility, schema, etc.)4723. Extract elements following the Extraction Rules above4734. Assign deterministic IDs using the `idx:<path>:<type>:<ordinal>` format4745. Store the `elements` object in the file's index entry4756. Set `lastIndexed` to the current timestamp4767. Set `fileModified` to the file's `mtime`477478**ID stability and ordinal assignment:**479480- The `ordinal` in `idx:<path>:<type>:<ordinal>` is the **order of appearance** in the source file (0-indexed). The first DataPoint found in a file is `:dp:0`, the second is `:dp:1`, etc.481- **Stability guarantee:** If a file is re-indexed and the elements appear in the same order, they receive the same IDs. Edges referencing those IDs remain valid across re-indexes.482- **Limitation:** If elements are reordered, added, or removed, ordinals shift for all subsequent elements of that type. An element that was `:dp:2` may become `:dp:1` after a deletion.483- **Stale edge handling:** During Step 5.8 (edge validation), any edge referencing an ID that no longer exists is removed. Before removal, attempt **label-based recovery**: search for a node with the same `label` in the same file. If found, log the ID change (`"dp:2 → dp:1 (label: User Email)"`) in the report. Do not auto-fix — report only, so the user can review.484- **Guidance:** Prefer `label` + file path for display and human communication. Use IDs only for edge references and internal lookups.485486### Step 5: Rebuild Aggregated Spec487488Merge all per-file elements into a single FlowSpec v1.2.0 spec:4894901. Collect all `dataPoints` from all files into `spec.dataPoints`4912. Collect all `components` from all files into `spec.components`4923. Collect all `transforms` from all files into `spec.transforms`4934. Collect all `tables` from all files into `spec.tables`4945. Collect all `images` from all files into `spec.images`4956. Collect all `dataFlow` edges from all files into `spec.dataFlow`4967. Collect all `screens` from all files into `spec.screens`4978. **Cross-file edge resolution** — 5-phase algorithm:498499 **Phase A: Build the import graph.** For every indexed file, parse its import statements. Resolve each import path to a relative file path in the index (handle `$lib/`, `@/`, `~/` aliases, `index.ts` barrel files, and `* as namespace` imports). Build a map: `{ importingFile → [{ importedFile, importedIdentifiers[] }] }`.500501 **Phase B: Match imported identifiers to indexed elements.** For each imported identifier (function name, component name, variable name), search the target file's extracted elements for a node whose `label` matches (case-insensitive). Record matches as `{ importingFileElement, importedFileElement, relationship }`.502503 **Phase C: Create edges based on element type × usage pattern.**504505 | Imported element type | Usage in importing file | Edge |506 |---|---|---|507 | Component | Rendered in template (`<Comp>`) | `from: parent-comp, to: child-comp, edgeType: "contains"` |508 | Transform (function) | Called with DataPoint args | `from: datapoint, to: transform, edgeType: "transforms"` |509 | DataPoint (exported var) | Read in template or logic | `from: datapoint, to: component, edgeType: "flows-to"` |510 | Table (store/API client) | Queried or mutated | `from: table, to: component/transform, edgeType: "flows-to"` |511512 **Phase D: Framework-specific patterns.** Check for these cross-file patterns and add appropriate edges:513 - **SvelteKit:** `+page.server.ts` `load()` return → `+page.svelte` `$props().data` — connect load's output DataPoints to page component's displays514 - **React/Next.js:** `getServerSideProps` / `loader` return → page component props — same pattern515 - **Vue:** `provide()` in parent → `inject()` in child — connect the provided DataPoint to the consuming component516 - **Shared stores:** Svelte stores (`writable`), React context, Zustand/Pinia — connect the store (Table node) to all components that subscribe517518 **Phase E: Deduplication.** Before adding any cross-file edge, check if an edge with the same `from`, `to`, and `edgeType` already exists. Skip duplicates.5195209. **Validate edges:** Remove any edge where `from` or `to` references an ID that no longer exists in the merged spec. Before removal, attempt label-based recovery (see ID stability section above).52110. **Populate `locations`:** For each Component in `spec.components`, iterate its `displays` array — for each DataPoint ID, add `{ "component": "<comp-id>", "role": "output" }` to that DataPoint's `locations`. Then iterate its `captures` array — for each DataPoint ID, add `{ "component": "<comp-id>", "role": "input" }` to that DataPoint's `locations`. Deduplicate entries with the same component + role.52211. Update `spec.metadata` with counts.523524### Step 6: Write Index File525526```bash527mkdir -p .flowspec528# Write the complete index529```530531Write the full index JSON to `.flowspec/codebase-index.json` with 2-space indentation.532533### Step 7: Report534535Print a summary:536537```538FlowSpec Codebase Index — complete539540 Files scanned: 15 (8 new, 4 updated, 3 skipped, 2 removed)541 DataPoints: 42542 Components: 12543 Transforms: 8544 Tables: 3545 Images: 7546 Screens: 5547 Edges: 31548549 Index: .flowspec/codebase-index.json550 Spec: .flowspec/codebase-index.json → spec (v1.2.0)551552 To import into FlowSpec:553 flowspec_create_project({ name: "<project> (indexed)" })554 # Then use the spec.dataPoints, spec.components, etc. with flowspec_create_node555```556557## Incremental Re-runs558559On subsequent invocations, the skill:5605611. Loads the existing index5622. Only processes files with `mtime > lastIndexed`5633. Preserves elements from unchanged files5644. Rebuilds the aggregated spec from all files (changed + unchanged)5655. Reports only what changed566567This makes re-indexing fast — a 500-file project where 3 files changed only reads those 3 files.568569## Edge Cases570571### Binary / Non-text Files572Skip silently. The file selection patterns already exclude most binary types.573574### Very Large Files575Index fully — every variable, every function, every data flow. Large files often contain the most important application logic. If a file is too large to read in one pass, read it in chunks and merge the extracted elements. Never skip or summarise.576577### Generated Files578Skip files that contain `@generated`, `// auto-generated`, or similar markers in the first 5 lines.579580### Monorepo / Multi-package581If a `workspaces` key is present in `package.json`, index each workspace separately and prefix IDs with the workspace name: `idx:<workspace>:<path>:<type>:<ordinal>`.582583### Empty Projects584If no indexable files are found, create the index with an empty `files` object and a null `spec`. Report: "No indexable source files found."585586## Using the Index with FlowSpec MCP587588### Create a New Project from Index589590```591flowspec_create_project({ name: "My App (indexed)" })592# Returns projectId593594# Create nodes — flowspec_create_node accepts: datapoint, component, transform, table, actor595for each dataPoint in spec.dataPoints:596 flowspec_create_node({ projectId, type: "datapoint", label: dataPoint.label, data: { ... } })597598for each component in spec.components:599 flowspec_create_node({ projectId, type: "component", label: component.label, data: { ... } })600601for each transform in spec.transforms:602 flowspec_create_node({ projectId, type: "transform", label: transform.description, data: { ... } })603604for each table in spec.tables:605 flowspec_create_node({ projectId, type: "table", label: table.label, data: { ... } })606607for each actor in spec.actors:608 flowspec_create_node({ projectId, type: "actor", label: actor.label, data: { actorType: actor.actorType, description: actor.description } })609610# Create screens — use flowspec_create_screen (NOT flowspec_create_node)611for each screen in spec.screens:612 flowspec_create_screen({ projectId, name: screen.name, layout: screen.layout })613 # Returns screenId614 for each region in screen.regions:615 flowspec_add_region({ projectId, screenId, label: region.label, position: region.position, size: region.size, elements: region.elements })616617# Upload images — use flowspec_upload_image (NOT flowspec_create_node)618for each image in spec.images:619 flowspec_upload_image({ projectId, label: image.label, url: image.url, width: image.width, height: image.height })620621# Create edges622for each edge in spec.dataFlow:623 flowspec_create_edge({ projectId, source: edge.from, target: edge.to, edgeType: edge.edgeType })624625# Auto-layout after import626flowspec_auto_layout({ projectId })627```628629> **Important:** `flowspec_create_node` accepts `datapoint`, `component`, `transform`, `table`, and `actor` as the `type` parameter. Screens and images have their own dedicated MCP tools.630631### Append to Existing Project632633```634flowspec_search_nodes({ projectId, query: "..." }) # Check what already exists635# Only create nodes/edges that don't already exist636```637638### Analyse After Import639640```641flowspec_analyse_project({ projectId })642# Reports orphan nodes, missing connections, duplicates643```644645## Configuration646647Optional `.flowspec/index-config.json` to override defaults:648649```json650{651 "include": ["src/**/*.ts", "src/**/*.svelte"],652 "exclude": ["src/**/*.test.ts", "src/**/*.spec.ts", "**/*.d.ts"],653 "maxFileSize": 100000,654 "skipGenerated": true655}656```657658If this file exists, use it. Otherwise, use the defaults from File Selection above.659660---661> Converted and distributed by [TomeVault](https://tomevault.io/claim/jktfe) — claim your Tome and manage your conversions.662<!-- tomevault:4.0:skill_md:2026-04-13 -->