Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/symbols-visibility-column.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@stainless-code/codemap": minor
---

`symbols.visibility` column — JSDoc visibility tag (`@public` / `@private` / `@internal` / `@alpha` / `@beta`) extracted at parse time and stored as a real column. Replaces the `LIKE '%@beta%'` regex in the `visibility-tags` recipe. `SCHEMA_VERSION` bumps from 3 to 4 — `.codemap.db` rebuilds automatically on next index. Helper `extractVisibility(doc)` exported from `parser.ts`. New partial index `idx_symbols_visibility` covers `WHERE visibility IS NOT NULL` queries.
34 changes: 18 additions & 16 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ Optional **`codemap.config.ts`** (default export: object or async factory) or **

**Fresh database:** the default CLI **`codemap`** (incremental) calls **`createSchema()`** in **`runCodemapIndex`** before **`getChangedFiles()`**, so the **`meta`** table exists before **`getMeta(..., "last_indexed_commit")`** runs on an empty **`.codemap.db`**.

Current schema version: **3** — see [Schema Versioning](#schema-versioning) for details.
Current schema version: **4** — see [Schema Versioning](#schema-versioning) for details.

All tables use `STRICT` mode. Tables marked with `WITHOUT ROWID` store data directly in the primary key B-tree. PRAGMAs and index design: [SQLite Performance Configuration](#sqlite-performance-configuration).

Expand All @@ -179,21 +179,22 @@ All tables use `STRICT` mode. Tables marked with `WITHOUT ROWID` store data dire

### `symbols` — Functions, constants, classes, interfaces, types, enums (`STRICT`)

| Column | Type | Description |
| ----------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | INTEGER PK | Auto-increment row id |
| file_path | TEXT FK | References `files(path)` ON DELETE CASCADE |
| name | TEXT | Symbol name |
| kind | TEXT | `function`, `const`, `class`, `interface`, `type`, `enum`, `method`, `property`, `getter`, `setter` (last four are class members) |
| line_start | INTEGER | Start line (1-based) |
| line_end | INTEGER | End line |
| signature | TEXT | Reconstructed signature with generics and return types (e.g. `identity<T>(val): T`, `interface Repo<T> extends Iterable<T>`, `class Store<T> extends Base<T> implements IStore<T>`) |
| is_exported | INTEGER | 1 if exported |
| is_default_export | INTEGER | 1 if default export |
| members | TEXT | JSON array of enum members (NULL for non-enums). Each entry: `{"name":"…","value":"…"}` (value omitted for implicit-value enums) |
| doc_comment | TEXT | Leading JSDoc comment text (cleaned: `*` prefixes stripped, trimmed). NULL when absent. Preserves `@deprecated`, `@param`, etc. tags |
| value | TEXT | Literal value for `const` declarations (strings, numbers, booleans, `null`). NULL for non-literal or non-const symbols. Handles `as const` and simple template literals |
| parent_name | TEXT | Name of the enclosing symbol (class, function) for nested symbols. NULL for top-level (module scope). Class methods/properties point to their class |
| Column | Type | Description |
| ----------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| id | INTEGER PK | Auto-increment row id |
| file_path | TEXT FK | References `files(path)` ON DELETE CASCADE |
| name | TEXT | Symbol name |
| kind | TEXT | `function`, `const`, `class`, `interface`, `type`, `enum`, `method`, `property`, `getter`, `setter` (last four are class members) |
| line_start | INTEGER | Start line (1-based) |
| line_end | INTEGER | End line |
| signature | TEXT | Reconstructed signature with generics and return types (e.g. `identity<T>(val): T`, `interface Repo<T> extends Iterable<T>`, `class Store<T> extends Base<T> implements IStore<T>`) |
| is_exported | INTEGER | 1 if exported |
| is_default_export | INTEGER | 1 if default export |
| members | TEXT | JSON array of enum members (NULL for non-enums). Each entry: `{"name":"…","value":"…"}` (value omitted for implicit-value enums) |
| doc_comment | TEXT | Leading JSDoc comment text (cleaned: `*` prefixes stripped, trimmed). NULL when absent. Preserves `@deprecated`, `@param`, etc. tags |
| value | TEXT | Literal value for `const` declarations (strings, numbers, booleans, `null`). NULL for non-literal or non-const symbols. Handles `as const` and simple template literals |
| parent_name | TEXT | Name of the enclosing symbol (class, function) for nested symbols. NULL for top-level (module scope). Class methods/properties point to their class |
| visibility | TEXT | JSDoc visibility tag derived from `doc_comment` at parse time: `public` / `private` / `internal` / `alpha` / `beta`. NULL when no tag present. Tag must start its own line (after the JSDoc `*` prefix); first match in document order wins. Powers the `visibility-tags` recipe and `WHERE visibility = ?` queries via the partial index `idx_symbols_visibility` |

### `calls` — Function-scoped call edges, deduped per file (`STRICT`)

Expand Down Expand Up @@ -319,6 +320,7 @@ Uses the Rust-based `oxc-parser` via NAPI bindings to parse TypeScript/TSX/JS/JS

- **Symbols**: Functions, arrow functions, classes, interfaces, type aliases, enums — with reconstructed signatures including generic type parameters (e.g. `<T extends Base>`), return type annotations (e.g. `: Promise<void>`), class/interface heritage (`extends`, `implements`). Class methods, properties, getters, and setters are extracted as individual symbols with `parent_name` pointing to their class
- **JSDoc**: Leading `/** … */` comments attached to symbols via `doc_comment` column (cleaned: `*` prefixes stripped, tags preserved)
- **JSDoc visibility**: A line-leading `@public` / `@private` / `@internal` / `@alpha` / `@beta` tag is parsed once at extract time and stored in the `symbols.visibility` column — `WHERE visibility = 'beta'` becomes a structured query instead of a `LIKE '%@beta%'` regex. Backticked references inside prose (`@public` mentioned in a paragraph) intentionally don't match — the regex anchors on line-start. Helper: `extractVisibility(doc)` exported from `parser.ts`
- **Enum members**: String and numeric values for each member, stored as JSON in the `members` column (e.g. `[{"name":"Active","value":"active"}]`)
- **Const values**: Literal values (`string`, `number`, `boolean`, `null`, `as const`, simple template literals) stored in the `value` column
- **Type members**: Properties and method signatures of interfaces and object-literal type aliases, stored in the `type_members` table
Expand Down
2 changes: 1 addition & 1 deletion docs/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,7 @@ TS shape for one row of the `type_members` table.

### visibility tag

A JSDoc tag controlling export visibility — `@public`, `@internal`, `@private`, `@alpha`, `@beta`, `@deprecated`. Stored in `symbols.doc_comment`. The `visibility-tags` and `deprecated-symbols` recipes filter on these.
A JSDoc tag controlling export visibility — `@public`, `@internal`, `@private`, `@alpha`, `@beta`. Parsed from `doc_comment` at parse time (line-leading match) and stored in the `symbols.visibility` column (TEXT, NULL when no tag). The `visibility-tags` recipe filters on `WHERE visibility IS NOT NULL`. `@deprecated` is a related but separate JSDoc tag — surfaced via `WHERE doc_comment LIKE '%@deprecated%'` in the `deprecated-symbols` recipe (no dedicated column; deprecation is orthogonal to visibility, not a 6th value).

---

Expand Down
1 change: 1 addition & 0 deletions fixtures/golden/minimal/visibility-tags.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
{
"name": "_epochSeconds",
"kind": "function",
"visibility": "internal",
"file_path": "src/utils/date.ts",
"line_start": 12,
"signature": "_epochSeconds(): number",
Expand Down
11 changes: 4 additions & 7 deletions src/cli/query-recipes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,20 +174,17 @@ LIMIT 50`,
*/
"visibility-tags": {
description:
"Symbols tagged @internal / @private / @alpha / @beta in JSDoc",
sql: `SELECT name, kind, file_path, line_start, signature, doc_comment
"Symbols carrying a JSDoc visibility tag (public / private / internal / alpha / beta)",
sql: `SELECT name, kind, visibility, file_path, line_start, signature, doc_comment
FROM symbols
WHERE doc_comment LIKE '%@internal%'
OR doc_comment LIKE '%@private%'
OR doc_comment LIKE '%@alpha%'
OR doc_comment LIKE '%@beta%'
WHERE visibility IS NOT NULL
ORDER BY file_path ASC, line_start ASC
LIMIT 100`,
actions: [
{
type: "flag-non-public",
description:
"Treat as not part of the public API: don't import from package consumers; check the visibility tag before extending re-exports.",
"Treat as not part of the public API unless visibility = 'public': don't import from package consumers; check the visibility tag before extending re-exports.",
},
],
},
Expand Down
85 changes: 85 additions & 0 deletions src/db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { describe, expect, it } from "bun:test";

import {
closeDb,
createIndexes,
createTables,
getMeta,
getAllFileHashes,
insertFile,
insertSymbols,
SCHEMA_VERSION,
setMeta,
} from "./db";
Expand Down Expand Up @@ -32,4 +35,86 @@ describe("SQLite layer (in-memory)", () => {
closeDb(db);
}
});

it("symbols.visibility round-trips with index hit on WHERE visibility = ?", () => {
const db = openCodemapDatabase(":memory:");
try {
createTables(db);
createIndexes(db);
insertFile(db, {
path: "x.ts",
content_hash: "abc",
size: 1,
line_count: 1,
language: "ts",
last_modified: 0,
indexed_at: 0,
});
insertSymbols(db, [
{
file_path: "x.ts",
name: "publicFn",
kind: "function",
line_start: 1,
line_end: 1,
signature: "publicFn(): void",
is_exported: 1,
is_default_export: 0,
members: null,
doc_comment: "@public",
value: null,
parent_name: null,
visibility: "public",
},
{
file_path: "x.ts",
name: "internalFn",
kind: "function",
line_start: 2,
line_end: 2,
signature: "internalFn(): void",
is_exported: 1,
is_default_export: 0,
members: null,
doc_comment: "@internal",
value: null,
parent_name: null,
visibility: "internal",
},
{
file_path: "x.ts",
name: "plain",
kind: "function",
line_start: 3,
line_end: 3,
signature: "plain(): void",
is_exported: 1,
is_default_export: 0,
members: null,
doc_comment: null,
value: null,
parent_name: null,
visibility: null,
},
]);

const rows = db
.query("SELECT name, visibility FROM symbols ORDER BY name")
.all() as Array<{ name: string; visibility: string | null }>;
expect(rows).toEqual([
{ name: "internalFn", visibility: "internal" },
{ name: "plain", visibility: null },
{ name: "publicFn", visibility: "public" },
]);

const tagged = db
.query(
"SELECT name FROM symbols WHERE visibility IS NOT NULL ORDER BY name",
)
.all() as Array<{ name: string }>;
expect(tagged.map((r) => r.name)).toEqual(["internalFn", "publicFn"]);
} finally {
closeDb(db);
}
});
});
18 changes: 14 additions & 4 deletions src/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { openCodemapDatabase } from "./sqlite-db";
import type { CodemapDatabase, BindValues } from "./sqlite-db";

/** Bump on any DDL change; `createSchema()` auto-rebuilds on mismatch. */
export const SCHEMA_VERSION = 3;
export const SCHEMA_VERSION = 4;

export type { CodemapDatabase };

Expand Down Expand Up @@ -46,7 +46,8 @@ export function createTables(db: CodemapDatabase) {
members TEXT,
doc_comment TEXT,
value TEXT,
parent_name TEXT
parent_name TEXT,
visibility TEXT
) STRICT;

CREATE TABLE IF NOT EXISTS imports (
Expand Down Expand Up @@ -152,6 +153,8 @@ export function createIndexes(db: CodemapDatabase) {
WHERE is_exported = 1;
CREATE INDEX IF NOT EXISTS idx_symbols_functions ON symbols(name, file_path, line_start, line_end, signature)
WHERE kind = 'function';
CREATE INDEX IF NOT EXISTS idx_symbols_visibility ON symbols(visibility, file_path, name, line_start)
WHERE visibility IS NOT NULL;

CREATE INDEX IF NOT EXISTS idx_imports_source ON imports(source, file_path);
CREATE INDEX IF NOT EXISTS idx_imports_resolved ON imports(resolved_path, file_path);
Expand Down Expand Up @@ -294,6 +297,12 @@ export interface SymbolRow {
doc_comment: string | null;
value: string | null;
parent_name: string | null;
/**
* JSDoc visibility tag: `public` / `private` / `internal` / `alpha` / `beta`.
* Null when the doc has no visibility tag (or no doc at all). First match
* in document order wins when multiple tags are present.
*/
visibility: string | null;
}

const BATCH_SIZE = 500;
Expand Down Expand Up @@ -326,8 +335,8 @@ export function insertSymbols(db: CodemapDatabase, symbols: SymbolRow[]) {
batchInsert(
db,
symbols,
"INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, is_exported, is_default_export, members, doc_comment, value, parent_name)",
"(?,?,?,?,?,?,?,?,?,?,?,?)",
"INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, is_exported, is_default_export, members, doc_comment, value, parent_name, visibility)",
"(?,?,?,?,?,?,?,?,?,?,?,?,?)",
(s, v) =>
v.push(
s.file_path,
Expand All @@ -342,6 +351,7 @@ export function insertSymbols(db: CodemapDatabase, symbols: SymbolRow[]) {
s.doc_comment,
s.value,
s.parent_name,
s.visibility,
),
);
}
Expand Down
Loading
Loading