# Phase 4: Static Analysis — TypeScript/JavaScript Adapters and Security

## Goal

Add TypeScript and JavaScript language adapters using tree-sitter, and build the security analysis layer (secret detection via `detect-secrets`, dependency CVE scanning via `pip-audit` and `npm audit`). After this phase, the analysis pipeline handles Python, TypeScript, and JavaScript codebases with import resolution, definition extraction, and entry point detection — plus flags hardcoded secrets and known dependency vulnerabilities. The security results feed into Phase 6's briefing generation as the "security posture summary."

## Dependencies

- **Phase 1** — `constants.py` (EntryType enum, InvestigationCategory)
- **Phase 2** — `models/analysis.py` (Module, FileSnapshot for persistence)
- **Phase 3** — `analysis/models.py` (ParsedFile, ParsedImport, FunctionDef, EntryPoint, AnalysisResult), `analysis/adapters/protocol.py` (LanguageAdapter protocol, AdapterRegistry), `analysis/orchestrator.py` (analyze_repository pipeline), `analysis/discovery.py` (AnalysisError)

## Research Notes

### Tree-sitter Python API (v0.23+)

- **Grammar packages:** `tree-sitter-javascript` (PyPI, provides `.js`/`.jsx`) and `tree-sitter-typescript` (PyPI, provides `.ts`/`.tsx` as separate languages)
- **Language loading:** `Language(tree_sitter_javascript.language())` for JS, `Language(tree_sitter_typescript.language_typescript())` for TS, `Language(tree_sitter_typescript.language_tsx())` for TSX
- **Parser creation:** `Parser(language)` — parse with `parser.parse(source_bytes)`
- **Tree walking:** Depth-first via `TreeCursor` — `cursor.goto_first_child()`, `cursor.goto_next_sibling()`, `cursor.goto_parent()`. Each node has `.type`, `.text`, `.start_point`, `.end_point`, `.child_by_field_name()`, `.children`, `.named_children`
- **Import node types:** `import_statement` (ES modules), `call_expression` with function `require` (CommonJS)
- **Function node types:** `function_declaration`, `generator_function_declaration`, `arrow_function` (inside `variable_declarator`), `method_definition` (inside `class_body`)
- **Class node types:** `class_declaration`, `abstract_class_declaration` (TS only)
- **TS-specific types:** `interface_declaration`, `type_alias_declaration`, `enum_declaration`
- **Export node types:** `export_statement` (ES modules), `assignment_expression` with `module.exports` / `exports.*` (CommonJS)
- **Entry point detection:** Express/Koa/Fastify app creation, `app.listen()` calls, default exports in index files, `if (require.main === module)` pattern

### JS/TS import resolution

- **Relative imports** (`./foo`, `../bar`): Try extensions in order: `.ts`, `.tsx`, `.js`, `.jsx`, then `/index.ts`, `/index.tsx`, `/index.js`, `/index.jsx`
- **Bare imports** (`express`, `@scope/pkg`): External packages → return None
- **Path aliases** (`@/components/...`): Would require tsconfig.json parsing — **out of scope for MVP** (known limitation, noted in contracts)
- Resolution is simpler than Python's because JS/TS always uses explicit relative paths (`.` or `..` prefix) for local imports

### detect-secrets programmatic API

- `SecretsCollection` is the main entry point
- `scan_file(filename)` scans one file — there is NO `scan_files` plural method. Call in a loop for multiple files
- Must wrap calls in `default_settings()` context manager
- `secrets.json()` returns a dict with a `"results"` wrapper key: `{"version": ..., "results": {filename: [findings]}, ...}`. Access findings via `secrets.json()["results"]`, not at the root level. Each finding has `type`, `hashed_secret`, `line_number`, `is_verified`
- Can scan specific files (no need to scan entire directory)

### Dependency vulnerability scanning

- **pip-audit:** Best invoked via subprocess for API stability. **The `-r` flag only accepts requirements.txt format — it does NOT work with `pyproject.toml`.** For requirements.txt: `pip-audit -r requirements.txt -f json`. For pyproject.toml: `pip-audit --locked .` (run with `cwd=repo_path`, no `-r` flag). JSON output is an array of `{name, version, vulns: [{id, fix_versions, aliases, description}]}`. Exits non-zero when vulns found — parse stdout regardless
- **npm audit:** CLI-only, invoke via `subprocess.run(["npm", "audit", "--json"], cwd=project_dir)`. Requires `package-lock.json` (or `yarn.lock`) to be present — check for lockfile existence before invoking. JSON output (v7+) has `vulnerabilities` dict keyed by package name with `severity`, `isDirect`, `via` (advisory details), `fixAvailable`. The `via` entries can be objects or strings — must check type
- Python scanning requires `requirements.txt` (use `-r`) or `pyproject.toml` (use `--locked`). JS scanning requires both `package.json` AND a lockfile (`package-lock.json` or `yarn.lock`)

### Security considerations

- `detect-secrets` does not execute target code — it's regex/entropy-based scanning
- `pip-audit` and `npm audit` make network calls to vulnerability databases (PyPI, npm registry) — security scanning is **not offline**. If network unavailable, scanning fails gracefully and results are empty
- Subprocess calls use list arguments (no shell injection)
- Secret values are never stored — `detect-secrets` returns hashed values only

### Adapter architecture decision

The TypeScript and JavaScript adapters share ~80% of their parsing logic (tree-sitter traversal, import extraction, definition extraction, entry point detection). A shared internal module (`_treesitter_utils.py`) provides the common functions. Each adapter class is thin — it selects the correct tree-sitter language, registers its extensions, and delegates to the shared module. This avoids duplication without over-engineering (the shared module has two real consumers).

JavaScript adds CommonJS-specific handling: `require()` import detection and `module.exports`/`exports.*` entry point detection.

---

## Tasks

### Task 1: Add tree-sitter grammar dependencies

**Files:** `pyproject.toml`

**Action:** Add `tree-sitter-javascript` and `tree-sitter-typescript` to the `dependencies` list. These are the grammar packages that provide compiled language bindings for tree-sitter.

Add:
- `"tree-sitter-javascript>=0.23"`
- `"tree-sitter-typescript>=0.23"`

**Verify:** `pip install -e ".[dev]"` — confirm packages install without errors.

**Done when:** Both grammar packages are declared in pyproject.toml and installable.

---

### Task 2: Tree-sitter shared parsing utilities

**Files:** `src/unfamiliar/analysis/adapters/_treesitter_utils.py`

**Action:** Create an internal module with shared functions for extracting structured data from tree-sitter parse trees. These functions operate on tree-sitter `Node` objects and return the same dataclasses used by the Python adapter (ParsedFile, ParsedImport, FunctionDef, EntryPoint). The module is prefixed with `_` to indicate it's internal to the adapters package.

Functions:

- `extract_es_imports(root: Node, source_path: str) -> list[ParsedImport]` — Walk the tree for `import_statement` nodes. Extract the source string (module path) and imported names from `import_clause`/`named_imports`/`namespace_import`. Set `is_relative` based on whether the source starts with `.`
- `extract_require_imports(root: Node, source_path: str) -> list[ParsedImport]` — Walk for `call_expression` nodes where the function is `require`. Extract the argument string as the import target. Set `is_relative` based on `.` prefix. Used only by the JavaScript adapter
- `extract_definitions(root: Node) -> list[FunctionDef]` — Walk for `function_declaration`, `generator_function_declaration`, `class_declaration`, `abstract_class_declaration`, `method_definition`, and arrow functions inside `variable_declarator`. Extract name, line range, async flag, kind. Methods inside classes get `kind="method"` with `ClassName.methodName` naming
- `extract_entry_points(root: Node, file_path: str) -> list[EntryPoint]` — Detect: Express/Fastify/Koa app creation (`EntryType.API_ROUTE`), `app.listen()` calls (`EntryType.API_ROUTE`), default export in index files (`EntryType.MAIN`), `if (require.main === module)` pattern (`EntryType.MAIN`)
- `calculate_complexity(root: Node) -> int` — Count branching nodes: `if_statement`, `for_statement`, `for_in_statement`, `while_statement`, `do_statement`, `switch_case`, `catch_clause`, `ternary_expression`, `&&`/`||` in binary expressions
- `traverse(root: Node) -> Generator[Node, None, None]` — Depth-first tree traversal using TreeCursor. Yields every node in the tree

**Verify:** `ruff check`; `mypy`

**Done when:** All shared functions are importable and type-check. Functions operate on tree-sitter Node objects and return analysis dataclasses.

---

### Task 3: TypeScript adapter

**Files:** `src/unfamiliar/analysis/adapters/typescript_adapter.py`

**Action:** Implement the TypeScript language adapter using tree-sitter. Handles `.ts` and `.tsx` files.

Class: `TypeScriptAdapter` (implements `LanguageAdapter` protocol)

- `supported_extensions() -> frozenset[str]` — returns `frozenset({".ts", ".tsx"})`
- `can_handle(path: str) -> bool` — checks extension is `.ts` or `.tsx`
- `parse_file(path: Path, content: bytes) -> ParsedFile`:
  1. Select correct tree-sitter language: `.tsx` → `language_tsx()`, `.ts` → `language_typescript()`
  2. Parse content with `Parser(language).parse(content)`
  3. Extract imports via `extract_es_imports()`
  4. Extract definitions via `extract_definitions()` (includes TS-specific: interfaces, type aliases, enums)
  5. Detect entry points via `extract_entry_points()`
  6. Calculate complexity via `calculate_complexity()`
  7. Handle parse errors gracefully — return empty ParsedFile on failure
- `resolve_import(importing_file: str, import_target: str, repo_files: set[str]) -> str | None`:
  1. If target starts with `.` or `..` — relative import: resolve against importing file's directory. Try extensions in order: `.ts`, `.tsx`, `.js`, `.jsx`, then index files in subdirectory
  2. If bare import (no `.` prefix) — external package, return None
  3. Support `src/` layout via suffix matching (same pattern as Python adapter)

**Verify:** `ruff check`; `mypy`; manual: parse a TypeScript file, check imports/definitions extracted

**Done when:** TypeScript adapter parses `.ts`/`.tsx` files via tree-sitter, extracts imports/definitions/entry points, and resolves relative imports to repo file paths.

---

### Task 4: JavaScript adapter

**Files:** `src/unfamiliar/analysis/adapters/javascript_adapter.py`

**Action:** Implement the JavaScript language adapter using tree-sitter. Handles `.js` and `.jsx` files. Extends the TypeScript adapter's pattern with CommonJS support.

Class: `JavaScriptAdapter` (implements `LanguageAdapter` protocol)

- `supported_extensions() -> frozenset[str]` — returns `frozenset({".js", ".jsx"})`
- `can_handle(path: str) -> bool` — checks extension is `.js` or `.jsx`
- `parse_file(path: Path, content: bytes) -> ParsedFile`:
  1. Parse content with `Parser(JS_LANGUAGE).parse(content)` (single language for all JS/JSX)
  2. Extract imports: `extract_es_imports()` + `extract_require_imports()` (CommonJS)
  3. Extract definitions via `extract_definitions()`
  4. Detect entry points via `extract_entry_points()` + CommonJS entry detection (`module.exports` assignments, `exports.*` assignments)
  5. Calculate complexity via `calculate_complexity()`
  6. Handle parse errors gracefully
- `resolve_import(importing_file: str, import_target: str, repo_files: set[str]) -> str | None`:
  1. Same resolution logic as TypeScript adapter — relative imports try extensions, bare imports return None
  2. Extension search order: `.js`, `.jsx`, `.ts`, `.tsx`, then index files (JS projects may import TS files in mixed codebases)

**Verify:** `ruff check`; `mypy`; manual: parse a JavaScript file with both ES module and CommonJS imports

**Done when:** JavaScript adapter parses `.js`/`.jsx` files, handles both ES modules and CommonJS patterns, resolves imports.

---

### Task 5: Register adapters and update exports

**Files:** `src/unfamiliar/analysis/adapters/python_adapter.py` (update `create_default_registry`), `src/unfamiliar/analysis/adapters/__init__.py`

**Action:**
1. Update `create_default_registry()` in `python_adapter.py` to also register `TypeScriptAdapter` and `JavaScriptAdapter`. Import them locally inside the function to avoid circular imports
2. Update `__init__.py` to re-export new adapter classes

Registration order: PythonAdapter, TypeScriptAdapter, JavaScriptAdapter (Python first since it was the reference implementation)

**Verify:** `python -c "from unfamiliar.analysis.adapters import create_default_registry; r = create_default_registry(); print(r.supported_languages())"`

**Done when:** `create_default_registry()` returns a registry with all three adapters. `__init__.py` exports all adapter classes.

---

### Task 6: Security data structures

**Files:** `src/unfamiliar/analysis/models.py`

**Action:** Add dataclasses for security analysis results. Also add a `security` field to `AnalysisResult`.

New dataclasses:

- `SecretFinding` — a detected potential secret:
  - `file_path: str` (relative to repo root)
  - `line_number: int`
  - `secret_type: str` (e.g., "Base64 High Entropy String", "AWS Access Key")
  - `description: str` (human-readable summary)

- `VulnerabilityFinding` — a known CVE in a dependency:
  - `package_name: str`
  - `installed_version: str`
  - `vulnerability_id: str` (e.g., "CVE-2023-12345", "PYSEC-2023-456")
  - `severity: str` ("low", "moderate", "high", "critical")
  - `description: str`
  - `fix_version: str` (empty if no fix available)
  - `ecosystem: str` ("python" or "javascript")

- `SecurityResult` — aggregate security analysis:
  - `secrets: list[SecretFinding]` (default empty)
  - `vulnerabilities: list[VulnerabilityFinding]` (default empty)
  - `manifests_scanned: list[str]` (which manifest files were checked)
  - `scan_errors: list[str]` (errors encountered during scanning, e.g., "pip-audit not available")

Add to `AnalysisResult`:
- `security: SecurityResult` — use `field(default_factory=SecurityResult)` (not `= SecurityResult()`, which raises ValueError for mutable defaults in dataclasses). Matches the pattern used by all existing mutable fields in AnalysisResult.

**Verify:** `ruff check`; `mypy`; import check

**Done when:** Security dataclasses defined. AnalysisResult includes security field with default empty SecurityResult.

---

### Task 7: Secret detection module

**Files:** `src/unfamiliar/analysis/security/secrets.py`

**Action:** Implement secret detection using the `detect-secrets` library.

Functions:

- `scan_for_secrets(file_paths: list[str], repo_path: Path) -> list[SecretFinding]`:
  1. Create a `SecretsCollection` instance
  2. Within `default_settings()` context, call `secrets.scan_file(path)` in a loop — there is no `scan_files` plural method
  3. Convert results via `secrets.json()["results"]` (findings are under the `"results"` key, not at the root) to `SecretFinding` dataclasses
  4. Skip binary files and very large files (>1MB) to avoid false positives on encoded data
  5. Return empty list on any unexpected error (log warning)

Do NOT update `__init__.py` in this task — Task 9 exports all security symbols together.

**Verify:** `ruff check`; `mypy`

**Done when:** Secret detection scans specified files and returns structured findings. Graceful degradation if detect-secrets is unavailable.

---

### Task 8: Dependency vulnerability scanning

**Files:** `src/unfamiliar/analysis/security/vulnerabilities.py`

**Action:** Implement dependency vulnerability scanning using `pip-audit` (Python) and `npm audit` (JS/TS) via subprocess.

Functions:

- `scan_vulnerabilities(repo_path: Path) -> tuple[list[VulnerabilityFinding], list[str], list[str]]`:
  Returns `(findings, manifests_scanned, errors)`.
  1. Check for `requirements.txt` at repo root → if found, run `_scan_requirements_txt()`
  2. Check for `pyproject.toml` at repo root → if found, run `_scan_pyproject_toml()`
  3. Check for JS manifest: `package.json` AND (`package-lock.json` OR `yarn.lock`) at repo root → if both present, run `_scan_js_deps()`
  4. Collect all findings, manifests scanned, and any errors

- `_scan_requirements_txt(manifest_path: Path) -> list[VulnerabilityFinding]`:
  1. Run `subprocess.run(["pip-audit", "-r", str(manifest_path), "-f", "json", "--progress-spinner=off"], ...)`
  2. Parse JSON output: extract package name, version, vulnerability ID, severity, description, fix versions
  3. Map aliases to find CVE identifiers where available
  4. If `pip-audit` not found or fails, return empty list and log error

- `_scan_pyproject_toml(repo_path: Path) -> list[VulnerabilityFinding]`:
  1. Run `subprocess.run(["pip-audit", "--locked", "-f", "json", "--progress-spinner=off"], cwd=repo_path, ...)`
  2. `-r` flag does NOT work with pyproject.toml — use `--locked` with `cwd=repo_path` instead
  3. Parse JSON output with same logic as `_scan_requirements_txt`
  4. If `pip-audit` not found or fails, return empty list and log error

- `_scan_js_deps(project_dir: Path) -> list[VulnerabilityFinding]`:
  1. Run `subprocess.run(["npm", "audit", "--json"], cwd=project_dir, ...)`
  2. Only call this if a lockfile (`package-lock.json` or `yarn.lock`) was found — npm audit fails without one
  3. Parse JSON output: extract from `vulnerabilities` dict — package name, severity, advisory details from `via` entries
  4. If `npm` not found or fails, return empty list and log error

**Verify:** `ruff check`; `mypy`

**Done when:** Vulnerability scanning detects manifests, invokes appropriate tools, parses JSON output, and returns structured findings. Graceful failure when tools aren't installed.

---

### Task 9: Integrate security and new adapters into orchestrator

**Files:** `src/unfamiliar/analysis/orchestrator.py`, `src/unfamiliar/analysis/security/__init__.py` (update exports)

**Action:**
1. Update `src/unfamiliar/analysis/security/__init__.py` to re-export `scan_for_secrets` and `scan_vulnerabilities`
2. Update `analyze_repository()` to include security scanning as a pipeline stage:
   - After file scoring (current Stage 8), add Stage 9: Security scanning
   - Call `scan_for_secrets()` with discovered file paths
   - Call `scan_vulnerabilities()` with repo_path
   - Assemble `SecurityResult` from findings
   - Attach to `AnalysisResult.security`
3. No changes needed for the adapter registration — `create_default_registry()` already returns all adapters after Task 5. The existing parsing loop in the orchestrator automatically uses whichever adapter the registry returns for each file

**Verify:** `ruff check`; `mypy`; manual: run `analyze_repository()` on a repo with mixed languages

**Done when:** The analysis pipeline discovers and parses TS/JS files alongside Python, and includes security results in the AnalysisResult.

---

### Task 10: Full verification

**Files:** All files created/modified in this phase.

**Action:**
1. `ruff check src/unfamiliar/` — full lint pass
2. `mypy src/unfamiliar/` — full type check
3. `python -c "from unfamiliar.analysis.adapters import TypeScriptAdapter, JavaScriptAdapter"` — verify imports
4. `python -c "from unfamiliar.analysis.adapters import create_default_registry; r = create_default_registry(); print(r.supported_languages())"` — verify all 3 adapters registered
5. `python -c "from unfamiliar.analysis.models import SecurityResult, SecretFinding, VulnerabilityFinding"` — verify security models
6. `python -c "from unfamiliar.analysis.security import scan_for_secrets, scan_vulnerabilities"` — verify security exports
7. `python -c "from unfamiliar.analysis import analyze_repository"` — verify no circular imports
8. Verify tree-sitter parsing works on a sample TS/JS snippet
9. Verify no regressions in Phase 1/2/3 code

**Verify:** All commands exit 0.

**Done when:** All lint, type checks, and imports pass. No regressions. New adapters and security scanning are fully integrated.

---

## Interface Contracts

These contracts define the public API for the Phase 4 additions. Phase 5 tests will be written against these contracts.

### Tree-sitter Shared Utilities (`src/unfamiliar/analysis/adapters/_treesitter_utils.py`)

#### `traverse(root: Node) -> Generator[Node, None, None]`
- **Purpose:** Depth-first traversal of a tree-sitter parse tree.
- **Invariants:** Yields every node in the tree exactly once. Root node is yielded first.
- **Edge cases:** Empty tree (yields only root). Very deeply nested tree (uses iterative cursor, not recursion — no stack overflow).

#### `extract_es_imports(root: Node, source_path: str) -> list[ParsedImport]`
- **Purpose:** Extract ES module import statements from a tree-sitter parse tree.
- **Invariants:** All returned imports have `source_path` matching the input. `is_relative` is True when the import source starts with `.` or `..`. `target` is the unquoted module path string.
- **Valid inputs:** Root node of a parsed JS or TS file.
- **Edge cases:** Side-effect imports (`import 'polyfill'` — names empty). Namespace imports (`import * as x` — names=["*"]). Type imports (`import type {X}` — still extracted, no distinction from value imports in MVP). Re-export imports (`export { x } from 'y'` — these are `export_statement` nodes, not captured here).

#### `extract_require_imports(root: Node, source_path: str) -> list[ParsedImport]`
- **Purpose:** Extract CommonJS `require()` calls from a tree-sitter parse tree.
- **Invariants:** Only captures `require()` calls where the argument is a string literal. `is_relative` is True when the argument starts with `.` or `..`.
- **Edge cases:** Dynamic require (`require(variable)` — not captured). Nested require inside if/try blocks (still captured). `require.resolve()` (not captured — different call).

#### `extract_definitions(root: Node) -> list[FunctionDef]`
- **Purpose:** Extract function, class, and method definitions.
- **Invariants:** `kind` is "function" for top-level functions and arrow functions, "class" for class declarations, "method" for methods inside class bodies. `line_start` and `line_end` are 1-indexed. `is_async` is True for functions with the `async` keyword.
- **Edge cases:** Anonymous arrow function assigned to a variable (name from variable). Generator functions (treated as regular functions). Abstract classes in TS (kind="class"). Interface/type/enum declarations in TS — **not captured** as FunctionDef (they are informational-only in TS; the adapter doesn't need to track them as definitions for scoring/import-graph purposes).

#### `extract_entry_points(root: Node, file_path: str) -> list[EntryPoint]`
- **Purpose:** Detect entry points in JS/TS files.
- **Invariants:** `entry_type` is a valid `EntryType` enum value. `line >= 1`.
- **Detection heuristics:**
  - `express()`/`Fastify()`/`new Koa()` app creation → `EntryType.API_ROUTE`
  - `app.listen()` call → `EntryType.API_ROUTE`
  - Default export in index file (`index.ts`/`index.js`) → `EntryType.MAIN`
  - `if (require.main === module)` → `EntryType.MAIN`
- **Edge cases:** File with multiple entry points (all captured). Non-index file with default export (not treated as entry point — too common in component files).

#### `calculate_complexity(root: Node) -> int`
- **Purpose:** Approximate cyclomatic complexity for JS/TS.
- **Invariants:** `complexity >= 0`. Counts: `if_statement`, `for_statement`, `for_in_statement`, `while_statement`, `do_statement`, `switch_case`, `catch_clause`, `ternary_expression`, `&&`/`||` binary expressions.
- **Edge cases:** Empty file (complexity=0). File with only type declarations (complexity=0).

---

### TypeScript Adapter (`src/unfamiliar/analysis/adapters/typescript_adapter.py`)

#### `TypeScriptAdapter`
```python
class TypeScriptAdapter:
    def supported_extensions(self) -> frozenset[str]: ...
    def can_handle(self, path: str) -> bool: ...
    def parse_file(self, path: Path, content: bytes) -> ParsedFile: ...
    def resolve_import(
        self, importing_file: str, import_target: str, repo_files: set[str]
    ) -> str | None: ...
```
- **Purpose:** Parse TypeScript source files using tree-sitter.
- **Invariants:** Returns valid ParsedFile for any `.ts`/`.tsx` file, even with syntax errors (graceful degradation). Uses `language_tsx()` for `.tsx` files and `language_typescript()` for `.ts` files. Import resolution returns None for bare imports (external packages).
- **Valid inputs:** `content` is raw bytes of a TS/TSX file. `path` is relative to repo root.
- **Error conditions:** Never raises. Logs warnings for parse errors and returns empty ParsedFile.
- **Edge cases:** File with JSX syntax in `.tsx` (handled by TSX language). File importing from `@/` path alias (not resolved — returns None, known limitation). File with `declare module` statements (not extracted as definitions). Mixed TS/JS project where TS files import JS files (resolution tries both extension sets).

#### `resolve_import` specifics
- **Relative imports** (`.`/`..` prefix): Resolve against importing file's directory. Try extensions in order: `.ts`, `.tsx`, `.js`, `.jsx`. Then try `/{target}/index.{ts,tsx,js,jsx}`. Support `src/` layout via suffix matching.
- **Bare imports** (no `.` prefix): Return None (external package).
- **Known limitation:** Path aliases (`@/`, `~/`, `#/`) from tsconfig.json are not resolved.

---

### JavaScript Adapter (`src/unfamiliar/analysis/adapters/javascript_adapter.py`)

#### `JavaScriptAdapter`
```python
class JavaScriptAdapter:
    def supported_extensions(self) -> frozenset[str]: ...
    def can_handle(self, path: str) -> bool: ...
    def parse_file(self, path: Path, content: bytes) -> ParsedFile: ...
    def resolve_import(
        self, importing_file: str, import_target: str, repo_files: set[str]
    ) -> str | None: ...
```
- **Purpose:** Parse JavaScript source files using tree-sitter. Extends ES module parsing with CommonJS support.
- **Invariants:** Same as TypeScriptAdapter, plus: captures `require()` calls as imports. Detects `module.exports` and `exports.*` as entry point signals.
- **Error conditions:** Never raises. Same graceful degradation as TypeScriptAdapter.
- **Edge cases:** File mixing ES modules and CommonJS (both patterns captured). File using `require()` with a variable argument (not captured — only string literals). `module.exports = class Foo {}` (detected as entry point).

#### `resolve_import` specifics
- Same resolution logic as TypeScriptAdapter but with extension order `.js`, `.jsx`, `.ts`, `.tsx` (JS extensions tried first since this is a JS adapter).

---

### Secret Detection (`src/unfamiliar/analysis/security/secrets.py`)

#### `scan_for_secrets(file_paths: list[str], repo_path: Path) -> list[SecretFinding]`
- **Purpose:** Scan source files for hardcoded secrets using detect-secrets.
- **Invariants:** `file_paths` are relative to `repo_path`. Returns only findings from files that exist and are readable. Never raises — returns empty list on any error.
- **Valid inputs:** `file_paths` from the discovery stage. `repo_path` is the git repo root.
- **Error conditions:** If `detect-secrets` is not installed, logs warning and returns `[]`. If a file can't be read, skips it.
- **Edge cases:** No files provided (returns `[]`). File with only false-positive secrets (still reported — detect-secrets handles its own filtering). Very large files (>1MB skipped to avoid noise from encoded data).

---

### Dependency Vulnerability Scanning (`src/unfamiliar/analysis/security/vulnerabilities.py`)

#### `scan_vulnerabilities(repo_path: Path) -> tuple[list[VulnerabilityFinding], list[str], list[str]]`
- **Purpose:** Scan dependency manifests for known vulnerabilities.
- **Returns:** `(findings, manifests_scanned, errors)`. `manifests_scanned` lists which files were checked. `errors` lists any tool failures.
- **Invariants:** Only scans manifests that exist at repo root. Never raises. Tools are invoked via subprocess with list arguments (no shell injection).
- **Valid inputs:** `repo_path` must be an existing directory (not necessarily a git repo — security scanning doesn't need git).
- **Error conditions:** If `pip-audit` is not installed, appends to errors and skips Python scanning. If `npm` is not installed, appends to errors and skips JS scanning. Network failures are caught and reported as errors.
- **Edge cases:** Repo with no manifests (empty results, empty manifests_scanned). Repo with both Python and JS manifests (both scanned). pip-audit finding with no CVE alias (uses PYSEC ID instead). npm audit finding where `via` is a string reference (skipped, only object advisories parsed). JS project without a lockfile (npm audit not attempted — no lockfile means npm audit would fail).

---

### Security Data Structures (`src/unfamiliar/analysis/models.py`)

#### `SecretFinding`
```python
@dataclass(frozen=True)
class SecretFinding:
    file_path: str       # Relative to repo root
    line_number: int     # 1-indexed
    secret_type: str     # Plugin name (e.g., "Base64 High Entropy String")
    description: str     # Human-readable summary
```
- **Invariants:** `line_number >= 1`. `file_path` is relative to repo root.

#### `VulnerabilityFinding`
```python
@dataclass(frozen=True)
class VulnerabilityFinding:
    package_name: str
    installed_version: str
    vulnerability_id: str   # CVE-*, PYSEC-*, GHSA-*
    severity: str           # "low", "moderate", "high", "critical"
    description: str
    fix_version: str        # Empty string if no fix available
    ecosystem: str          # "python" or "javascript"
```
- **Invariants:** `severity` is one of "low", "moderate", "high", "critical". `ecosystem` is "python" or "javascript".

#### `SecurityResult`
```python
@dataclass
class SecurityResult:
    secrets: list[SecretFinding]
    vulnerabilities: list[VulnerabilityFinding]
    manifests_scanned: list[str]
    scan_errors: list[str]
```
- **Invariants:** `secrets` and `vulnerabilities` default to empty lists. `scan_errors` captures tool-level failures (not individual finding errors).

---

## Property-Based Testing Candidates (for Phase 5)

1. **JS/TS import resolution symmetry** — for any relative import that resolves to a repo file, the resolved path exists in `repo_files`
2. **Extension trial exhaustiveness** — for any relative import target, all extension combinations are tried before returning None
3. **Adapter dispatch correctness** — `registry.get_adapter(path)` returns the correct adapter type for every supported extension
4. **ParsedFile language field consistency** — TypeScriptAdapter always returns `language="typescript"`, JavaScriptAdapter always returns `language="javascript"`
5. **Security result completeness** — `SecurityResult.manifests_scanned` contains every manifest file that exists in the repo
6. **Complexity non-negativity** — `calculate_complexity()` always returns `>= 0` for any valid parse tree
7. **Import extraction source_path invariant** — all ParsedImport objects returned have `source_path` matching the parsed file's path

---

## Security Checklist

- [x] **No code execution:** Tree-sitter parses but does not execute JS/TS code
- [x] **Subprocess safety:** `pip-audit`, `npm`, and `git` called with list arguments, no `shell=True`
- [x] **Secret value protection:** detect-secrets returns hashed values only — actual secrets never stored
- [x] **Path traversal prevention:** All paths are relative to repo root
- [x] **Encoding safety:** tree-sitter works with raw bytes — no decode needed for parsing
- [x] **Graceful degradation:** Missing tools (pip-audit, npm, detect-secrets) produce warnings, not crashes
- [x] **Network awareness:** Vulnerability scanning requires network — documented, fails gracefully when offline

---

## Issues Discovered During Verification Stage

### Pre-existing issues (not introduced in Phase 4)

1. **Line length violations in db/repositories/** (Phase 2 code)
   - Files: `campaign.py`, `chain.py`, `mission.py`, `module.py`
   - Issue: Lines exceeding 88 characters in create() method return statements
   - Status: Non-blocking, did not fix to avoid scope creep
   - Note: These were present before Phase 4 and do not affect functionality

