---
phase: 15-core-ast-chunker
plan: 01
type: tdd
wave: 1
depends_on: []
files_modified:
  - tests/ingest/test_chunker.py
autonomous: true
requirements:
  - TEST-01
  - TEST-02

must_haves:
  truths:
    - "TestChunkTypeScript tests exist and fail RED before implementation"
    - "TestChunkFile dispatch tests cover all four extensions (.ts, .tsx, .js, .jsx)"
    - "Every requirement for chunk_typescript has a failing test assertion before code is written"
  artifacts:
    - path: "tests/ingest/test_chunker.py"
      provides: "TestChunkTypeScript class + updated TestChunkFile"
      contains: "TestChunkTypeScript"
  key_links:
    - from: "tests/ingest/test_chunker.py"
      to: "src/corpus_analyzer/ingest/ts_chunker.py"
      via: "import chunk_typescript"
      pattern: "from corpus_analyzer.ingest.ts_chunker import chunk_typescript"
---

<objective>
Write all failing tests for chunk_typescript() before the implementation exists.

Purpose: TDD contract — RED phase establishes what chunk_typescript() must do; GREEN phase in Plan 02 makes it pass.
Output: tests/ingest/test_chunker.py extended with TestChunkTypeScript class (15 test methods) and updated TestChunkFile dispatch assertions for all 4 extensions.
</objective>

<execution_context>
@/home/ollie/.claude/get-shit-done/workflows/execute-plan.md
@/home/ollie/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/ROADMAP.md
@.planning/phases/15-core-ast-chunker/15-CONTEXT.md
@.planning/phases/15-core-ast-chunker/15-RESEARCH.md
@src/corpus_analyzer/ingest/chunker.py
@tests/ingest/test_chunker.py
</context>

<feature>
  <name>chunk_typescript TDD contract</name>
  <files>
    tests/ingest/test_chunker.py
  </files>
  <behavior>
    chunk_typescript(path: Path) -> list[dict[str, Any]] must satisfy:

    Node type extraction (8 types):
    - function foo(): void {} → chunk_name "foo", start_line 1
    - function* gen() {} → chunk_name "gen"
    - class Bar {} → chunk_name "Bar"
    - abstract class Abs {} → chunk_name "Abs"
    - interface IFoo {} → chunk_name "IFoo"
    - type Alias = string → chunk_name "Alias"
    - const fn = () => {} → chunk_name "fn" (lexical_declaration)
    - enum Color {} → chunk_name "Color"

    Export unwrapping:
    - export function foo(): void {} → text contains "export function foo", chunk_name "foo"
    - export class Bar {} → text contains "export class Bar", chunk_name "Bar"
    - export default function() {} → chunk_name "default"
    - export default class {} → chunk_name "default"

    Line accuracy:
    - start_line and end_line are 1-indexed
    - function on line 3 → start_line == 3

    JSDoc inclusion:
    - /** doc */ immediately preceding construct → start_line is doc's line, text includes "/** doc */"
    - // comment preceding construct → NOT included (single-line comments excluded)
    - blank line between JSDoc and declaration → JSDoc NOT included

    Grammar dispatch:
    - .tsx file with JSX syntax parses without fallback
    - .jsx file with JSX syntax parses without fallback

    Error handling:
    - has_error on partial tree (valid function + bad syntax) → does NOT fall back; still extracts good constructs
    - truly catastrophic parse exception → falls back to chunk_lines, returns list (not raises)

    Caching (IDX-07):
    - calling chunk_typescript twice on same-dialect files → _get_cached_parser called once per dialect

    chunk_name field:
    - every chunk dict has "chunk_name" key

    Dispatch (TEST-02 — update existing tests):
    - chunk_file(ts_file) routes to chunk_typescript (NOT chunk_lines)
    - chunk_file(tsx_file) routes to chunk_typescript
    - chunk_file(js_file) routes to chunk_typescript
    - chunk_file(jsx_file) routes to chunk_typescript
    - existing test_dispatches_other_to_chunk_lines updated to NOT expect .ts to be line-based

    Cases → expected outputs:
    - "function foo(): void {}\n" → [{"chunk_name": "foo", "start_line": 1, "end_line": 1, "text": "function foo(): void {}"}]
    - "export function foo(): void {}\n" → [{"chunk_name": "foo", "text": contains "export function foo"}]
    - "/** doc */\nfunction foo() {}\n" → start_line == 1, text starts with "/** doc */"
    - "// comment\nfunction foo() {}\n" → start_line == 2, text does NOT include "// comment"
    - "function good(): void {}\n@@@@invalid\n" → len(chunks) >= 1 and chunks[0]["chunk_name"] == "good"
    - b"\x00\x01\x02\x03" * 100 written as bytes then read → isinstance(result, list) (no raise)
    - non-ASCII: "function héllo(): void {}\n" → chunk_name == "héllo"
    - JSX in .tsx: "const el = () => <div />\n" (type: lexical_declaration) → chunk_name == "el"
  </behavior>
  <implementation>
    Add imports at top of test file:
      from corpus_analyzer.ingest.ts_chunker import chunk_typescript
      from corpus_analyzer.ingest.chunker import chunk_file, chunk_lines, chunk_markdown, chunk_python

    Add TestChunkTypeScript class after TestChunkLines, before TestChunkFile.

    Test methods (all use tmp_path fixture):
      1. test_function_extraction — plain function, checks chunk_name and start_line
      2. test_generator_extraction — function* gen(), checks chunk_name
      3. test_class_extraction — class Bar {}, checks chunk_name
      4. test_abstract_class_extraction — abstract class Abs {}, checks chunk_name
      5. test_interface_extraction — interface IFoo {}, checks chunk_name
      6. test_type_alias_extraction — type Alias = string, checks chunk_name
      7. test_lexical_declaration_extraction — const fn = () => {}, checks chunk_name "fn"
      8. test_enum_extraction — enum Color { R, G, B }, checks chunk_name "Color"
      9. test_export_function_unwrapping — export function foo(), checks "export function foo" in text and chunk_name "foo"
      10. test_export_class_unwrapping — export class Bar {}, checks "export class Bar" in text
      11. test_export_default_function — export default function() {}, chunk_name "default"
      12. test_line_range_accuracy — function on line 3 (two comment lines before), start_line == 3
      13. test_jsdoc_included — /** doc */ on line 1, function on line 2; start_line == 1, text includes "/** doc */"
      14. test_single_line_comment_not_included — "// comment\nfunction foo() {}\n"; start_line == 2
      15. test_jsx_in_tsx_parses — write "const el = (): JSX.Element => <div />;\n" to test.tsx; check no fallback (chunk_name == "el")
      16. test_jsx_in_jsx_parses — same but test.jsx extension; check chunk_name == "el"
      17. test_non_ascii_identifier — "function héllo(): void {}\n", chunk_name == "héllo"
      18. test_has_error_does_not_fall_back — "function good(): void {}\n@@@@invalid@@@@\n"; assert any(c["chunk_name"] == "good" for c in chunks)
      19. test_catastrophic_failure_falls_back — write b"\x00\x01\x02" * 100 to test.ts; chunks is list (no raise)
      20. test_chunk_name_field_present — function foo(); every chunk has "chunk_name" key
      21. test_parser_cache — call chunk_typescript twice with .ts files; no error (cache integrity)

    Update TestChunkFile:
      - Rename test_dispatches_other_to_chunk_lines → test_dispatches_ts_to_chunk_typescript
        (keep same name but change assertion: verify chunk has AST-extracted content for a named function)
      - Add test_dispatches_tsx_to_chunk_typescript — .tsx file with a function
      - Add test_dispatches_js_to_chunk_typescript — .js file with a function
      - Add test_dispatches_jsx_to_chunk_typescript — .jsx file with a function

    RED verification: run tests BEFORE implementation; import will fail with ImportError or ModuleNotFoundError.
    Then commit: test(15-01): add failing tests for chunk_typescript
  </implementation>
</feature>

<tasks>

<task type="auto">
  <name>Task 1: Write RED tests — TestChunkTypeScript + updated TestChunkFile</name>
  <files>tests/ingest/test_chunker.py</files>
  <action>
    Extend tests/ingest/test_chunker.py (do NOT rewrite existing tests). Add:

    1. At the top of the file, add import for chunk_typescript (this will be RED — module doesn't exist yet):
       from corpus_analyzer.ingest.ts_chunker import chunk_typescript

    2. Between TestChunkLines and TestChunkFile, add TestChunkTypeScript class with the following 21 test methods (all take self, tmp_path: Path):

       test_function_extraction: Write "function foo(): void {\n  console.log('hi');\n}\n" to tmp_path/"test.ts"; assert len(chunks)==1, chunks[0]["chunk_name"]=="foo", chunks[0]["start_line"]==1

       test_generator_extraction: "function* gen(): Generator<number, void, void> {\n  yield 1;\n}\n" → chunk_name=="gen"

       test_class_extraction: "class Bar {\n  x = 1;\n}\n" → chunk_name=="Bar"

       test_abstract_class_extraction: "abstract class Abs {\n  abstract method(): void;\n}\n" → chunk_name=="Abs"

       test_interface_extraction: "interface IFoo {\n  x: number;\n}\n" → chunk_name=="IFoo"

       test_type_alias_extraction: "type Alias = string | number;\n" → chunk_name=="Alias"

       test_lexical_declaration_extraction: "const fn = (): void => {};\n" → chunk_name=="fn"

       test_enum_extraction: "enum Color {\n  Red,\n  Green,\n  Blue,\n}\n" → chunk_name=="Color"

       test_export_function_unwrapping: "export function foo(): void {}\n" → len==1, "export function foo" in chunks[0]["text"], chunks[0]["chunk_name"]=="foo"

       test_export_class_unwrapping: "export class Bar {}\n" → "export class Bar" in chunks[0]["text"], chunks[0]["chunk_name"]=="Bar"

       test_export_default_function: "export default function(): void {}\n" → chunks[0]["chunk_name"]=="default"

       test_line_range_accuracy: "// line 1\n// line 2\nfunction foo(): void {}\n" → chunks[0]["start_line"]==3 (function starts on line 3; single-line comments are NOT included)

       test_jsdoc_included: "/** JSDoc comment */\nfunction foo(): void {}\n" → chunks[0]["start_line"]==1, "/** JSDoc comment */" in chunks[0]["text"]

       test_single_line_comment_not_included: "// not jsdoc\nfunction foo(): void {}\n" → chunks[0]["start_line"]==2 (function line), "// not jsdoc" not in chunks[0]["text"]

       test_jsdoc_blank_line_not_included: "/** doc */\n\nfunction foo(): void {}\n" → chunks[0]["start_line"]==3 (blank line between doc and decl → doc NOT included)

       test_jsx_in_tsx_parses: write "const el = (): JSX.Element => <div />;\n" to tmp_path/"test.tsx"; chunks[0]["chunk_name"]=="el" (JSX parsed without fallback)

       test_jsx_in_jsx_parses: same content to tmp_path/"test.jsx"; chunks[0]["chunk_name"]=="el"

       test_non_ascii_identifier: "function héllo(): void {}\n" to test.ts → chunks[0]["chunk_name"]=="héllo"

       test_has_error_does_not_fall_back: "function good(): void {}\n@@@@invalid@@@@\n" → any(c["chunk_name"]=="good" for c in chunks) is True (partial tree, NOT fallen back)

       test_catastrophic_failure_falls_back: (tmp_path/"test.ts").write_bytes(b"\x00\x01\x02\x03" * 100); result = chunk_typescript(tmp_path/"test.ts"); assert isinstance(result, list) (no exception raised)

       test_chunk_name_field_present: "function foo(): void {}\nconst x = 1;\n" → all("chunk_name" in c for c in chunks) is True

       test_parser_cache: call chunk_typescript twice on two different .ts files in tmp_path; assert both return list[dict] (verifies no cache-related crash)

    3. In TestChunkFile, make these changes:
       - Rename test_dispatches_other_to_chunk_lines → test_dispatches_ts_to_chunk_typescript
         New assertion: ts_file = tmp_path/"test.ts", write "function foo(): void {}\n", chunks = chunk_file(ts_file); assert any("foo" in c.get("chunk_name","") for c in chunks) — verifies AST dispatch (not line-based)
       - Add test_dispatches_tsx_to_chunk_typescript: tsx_file="test.tsx", "function bar(): void {}\n"; assert any("bar" in c.get("chunk_name","") for c in chunks)
       - Add test_dispatches_js_to_chunk_typescript: js_file="test.js", "function baz(): void {}\n"; assert any("baz" in c.get("chunk_name","") for c in chunks)
       - Add test_dispatches_jsx_to_chunk_typescript: jsx_file="test.jsx", "function qux(): void {}\n"; assert any("qux" in c.get("chunk_name","") for c in chunks)
  </action>
  <verify>
    <automated>cd /home/ollie/Development/Tools/vibe_coding/corpus-analyzer && uv run pytest tests/ingest/test_chunker.py::TestChunkTypeScript tests/ingest/test_chunker.py::TestChunkFile -x 2>&1 | head -30</automated>
    <manual>Confirm tests FAIL with ImportError or ModuleNotFoundError for corpus_analyzer.ingest.ts_chunker — this is the expected RED state. Existing tests (TestChunkMarkdown, TestChunkPython, TestChunkLines) must still pass.</manual>
    <sampling_rate>Run after this task; confirm RED before proceeding to Plan 02</sampling_rate>
  </verify>
  <done>
    - tests/ingest/test_chunker.py has TestChunkTypeScript class with 21 test methods
    - TestChunkFile has 4 dispatch tests for .ts/.tsx/.js/.jsx
    - uv run pytest tests/ingest/test_chunker.py::TestChunkTypeScript -x fails with ImportError (RED confirmed)
    - uv run pytest tests/ingest/test_chunker.py::TestChunkMarkdown tests/ingest/test_chunker.py::TestChunkPython tests/ingest/test_chunker.py::TestChunkLines -v passes (existing tests unbroken)
    - Commit: test(15-01): add failing tests for chunk_typescript
  </done>
</task>

</tasks>

<verification>
RED state confirmed:
  uv run pytest tests/ingest/test_chunker.py::TestChunkTypeScript -x  → FAILS (ImportError expected)
  uv run pytest tests/ingest/test_chunker.py::TestChunkMarkdown tests/ingest/test_chunker.py::TestChunkPython tests/ingest/test_chunker.py::TestChunkLines -v → PASSES

Plan 02 (implementation) is unblocked once this plan is committed.
</verification>

<success_criteria>
- 21 test methods in TestChunkTypeScript covering all 8 node types, export unwrapping, JSDoc, error handling, caching
- TestChunkFile updated with .ts/.tsx/.js/.jsx dispatch tests
- Tests fail RED with ImportError for ts_chunker module
- All existing tests still pass
- Committed to git
</success_criteria>

<output>
After completion, create `.planning/phases/15-core-ast-chunker/15-01-SUMMARY.md`
</output>
