---
phase: 01-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - pyproject.toml
  - .gitignore
  - .github/workflows/ci.yml
  - src/pcn_torch/__init__.py
  - src/pcn_torch/_types.py
  - src/pcn_torch/activations.py
  - src/pcn_torch/layers.py
  - tests/__init__.py
  - tests/test_activations.py
  - tests/test_layers.py
autonomous: true

must_haves:
  truths:
    - "`pip install -e .` succeeds without errors"
    - "`from pcn_torch import PCNLayer` succeeds and PCNLayer is an nn.Module subclass"
    - "`PCNLayer(784, 500, activation='relu')` constructs without error and `.weight` has shape (500, 784)"
    - "`layer(x)` where x is (32, 784) returns (prediction, preactivation) both of shape (32, 500)"
    - "`get_activation('relu')` returns a (fn, fn_deriv) pair; same for tanh, sigmoid, identity"
    - "Each activation derivative is hand-coded (no autograd) and mathematically correct"
    - "`ruff check .`, `ruff format --check .`, `mypy src/pcn_torch`, and `pytest tests/` all pass"
  artifacts:
    - path: "pyproject.toml"
      provides: "Build config (hatchling + hatch-vcs), tool configs (ruff, mypy, pytest)"
      contains: "hatchling"
    - path: "src/pcn_torch/__init__.py"
      provides: "Public API re-exports"
      exports: ["PCNLayer", "get_activation", "__version__"]
    - path: "src/pcn_torch/_types.py"
      provides: "Type aliases for activation functions"
      contains: "ActivationPair"
    - path: "src/pcn_torch/activations.py"
      provides: "Activation functions, hand-coded derivatives, registry"
      exports: ["get_activation"]
    - path: "src/pcn_torch/layers.py"
      provides: "PCNLayer nn.Module subclass"
      exports: ["PCNLayer"]
    - path: ".github/workflows/ci.yml"
      provides: "CI across Python 3.10-3.13 (lint, typecheck, test)"
    - path: "tests/test_activations.py"
      provides: "Tests for all activation functions and derivatives"
    - path: "tests/test_layers.py"
      provides: "Tests for PCNLayer construction, forward pass, shapes"
  key_links:
    - from: "src/pcn_torch/layers.py"
      to: "src/pcn_torch/activations.py"
      via: "get_activation() call in PCNLayer.__init__"
      pattern: "get_activation\\(activation\\)"
    - from: "src/pcn_torch/activations.py"
      to: "src/pcn_torch/_types.py"
      via: "type alias imports"
      pattern: "from pcn_torch._types import"
    - from: "src/pcn_torch/__init__.py"
      to: "src/pcn_torch/layers.py"
      via: "re-export PCNLayer"
      pattern: "from pcn_torch.layers import PCNLayer"
    - from: "pyproject.toml"
      to: "src/pcn_torch/_version.py"
      via: "hatch-vcs version-file hook"
      pattern: "version-file.*_version.py"
---

<objective>
Create the complete pcn-torch package scaffold with core types, activation functions (with hand-coded derivatives), and PCNLayer as a configurable nn.Module, plus CI and tests.

Purpose: Establish the installable foundation so that all subsequent phases can build on a working, tested, linted package. After this plan, developers can `pip install -e .` and `from pcn_torch import PCNLayer`.

Output: A fully configured Python package with pyproject.toml, src layout, CI workflow, activation registry, PCNLayer module, and passing tests.
</objective>

<execution_context>
@C:\Users\RDCHLEM1\.claude/get-shit-done/workflows/execute-plan.md
@C:\Users\RDCHLEM1\.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation/01-RESEARCH.md
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create package scaffold and configuration</name>
  <files>
    pyproject.toml
    .gitignore
    .github/workflows/ci.yml
    src/pcn_torch/__init__.py
    src/pcn_torch/_types.py
    tests/__init__.py
  </files>
  <action>
Create the package scaffold following the RESEARCH.md patterns exactly.

**pyproject.toml:**
- Build system: hatchling + hatch-vcs (requires = ["hatchling", "hatch-vcs"])
- Project name: "pcn-torch", dynamic version, description "Predictive Coding Networks in PyTorch"
- readme = "README.md", license = "MIT", requires-python = ">=3.10"
- dependencies = ["torch>=2.0"]
- [tool.hatch.version]: source = "vcs", fallback-version = "0.0.0.dev0"
- [tool.hatch.build.hooks.vcs]: version-file = "src/pcn_torch/_version.py"
- [tool.hatch.build.targets.wheel]: packages = ["src/pcn_torch"]
- Dev dependencies group [project.optional-dependencies] or [dependency-groups]: ruff>=0.15, mypy>=1.19, pytest>=9.0, pytest-cov>=7.0
- [tool.ruff]: target-version = "py310", line-length = 88, src = ["src"]
- [tool.ruff.lint]: select = ["E", "F", "W", "I", "UP", "B", "SIM", "TCH"]
- [tool.ruff.lint.isort]: known-first-party = ["pcn_torch"]
- [tool.mypy]: python_version = "3.10", warn_return_any = true, warn_unused_configs = true, disallow_untyped_defs = true
- [[tool.mypy.overrides]]: module = "torch.*", ignore_missing_imports = true
- [tool.pytest.ini_options]: testpaths = ["tests"], addopts = "-v --tb=short"
- [tool.coverage.run]: source_pkgs = ["pcn_torch"], branch = true

**.gitignore:**
- Add `src/pcn_torch/_version.py` to existing .gitignore (or create if needed)
- Include standard Python ignores: __pycache__, *.pyc, *.egg-info, dist/, build/, .eggs/, *.egg, .mypy_cache/, .ruff_cache/, .pytest_cache/, .coverage, htmlcov/

**.github/workflows/ci.yml:**
- Trigger on push to main and pull_request
- Three jobs: lint, typecheck, test
- lint job: checkout, setup-uv@v7 with cache, uv sync --dev, ruff check ., ruff format --check .
- typecheck job: checkout, setup-uv@v7 with cache, uv sync --dev, mypy src/pcn_torch
- test job: matrix python-version ["3.10", "3.11", "3.12", "3.13"], checkout, setup-uv@v7 with cache + python-version, uv sync --dev, pytest tests/ -v --cov=pcn_torch --cov-report=term-missing
- All jobs use: actions/checkout@v4, astral-sh/setup-uv@v7 with enable-cache: true and cache-dependency-glob: "uv.lock"

**src/pcn_torch/_types.py:**
- from __future__ import annotations
- Import TypeAlias from typing, Callable from collections.abc, Tensor from torch
- Define: ActivationFn: TypeAlias = Callable[[Tensor], Tensor]
- Define: ActivationDeriv: TypeAlias = Callable[[Tensor], Tensor]
- Define: ActivationPair: TypeAlias = tuple[ActivationFn, ActivationDeriv]

**src/pcn_torch/__init__.py:**
- Import and re-export: __version__ (from _version), PCNLayer (from layers), get_activation (from activations), ActivationFn/ActivationDeriv/ActivationPair (from _types)
- Define __all__ with all public names
- Handle ImportError for _version.py gracefully (it may not exist before first build): try/except with fallback __version__ = "0.0.0.dev0"

**tests/__init__.py:**
- Empty file (just a comment or blank)

After creating all files, run `uv sync --dev` to install the package in editable mode and generate _version.py.
  </action>
  <verify>
Run these commands and confirm all succeed:
1. `uv sync --dev` completes without errors
2. `python -c "from pcn_torch._types import ActivationFn, ActivationDeriv, ActivationPair; print('types OK')"` prints "types OK"
3. `python -c "from pcn_torch import __version__; print(__version__)"` prints a version string
  </verify>
  <done>
pyproject.toml exists with hatchling+hatch-vcs config. src/pcn_torch/ directory exists with __init__.py and _types.py. .github/workflows/ci.yml exists with 3 jobs. .gitignore includes _version.py. `uv sync --dev` succeeds and the package is importable.
  </done>
</task>

<task type="auto">
  <name>Task 2: Implement activation functions and PCNLayer</name>
  <files>
    src/pcn_torch/activations.py
    src/pcn_torch/layers.py
  </files>
  <action>
Implement the core modules following RESEARCH.md patterns exactly.

**src/pcn_torch/activations.py:**
- from __future__ import annotations
- Import torch, Tensor from torch, type aliases from pcn_torch._types
- Define named functions (NOT lambdas) for each activation and its derivative:
  - _relu(x: Tensor) -> Tensor: return torch.relu(x)
  - _relu_deriv(x: Tensor) -> Tensor: return (x > 0).float()
  - _tanh(x: Tensor) -> Tensor: return torch.tanh(x)
  - _tanh_deriv(x: Tensor) -> Tensor: t = torch.tanh(x); return 1.0 - t * t
  - _sigmoid(x: Tensor) -> Tensor: return torch.sigmoid(x)
  - _sigmoid_deriv(x: Tensor) -> Tensor: s = torch.sigmoid(x); return s * (1.0 - s)
  - _identity(x: Tensor) -> Tensor: return x
  - _identity_deriv(x: Tensor) -> Tensor: return torch.ones_like(x)
- Module-level registry: _ACTIVATIONS: dict[str, ActivationPair] mapping "relu", "tanh", "sigmoid", "identity" to their (fn, fn_deriv) pairs
- get_activation(name: str) -> ActivationPair: lookup in _ACTIVATIONS, raise ValueError with available names if not found

**src/pcn_torch/layers.py:**
- from __future__ import annotations
- Import torch, nn from torch, Tensor from torch, get_activation from pcn_torch.activations
- PCNLayer(nn.Module):
  - __init__(self, in_features: int, out_features: int, *, activation: str = "relu") -> None
    - super().__init__()
    - Look up activation: act_fn, act_deriv = get_activation(activation)
    - Store as self.activation_fn and self.activation_deriv (NOT as nn.Module attributes -- they are plain callables)
    - self.weight = nn.Parameter(torch.empty(out_features, in_features))
    - nn.init.xavier_uniform_(self.weight)
  - forward(self, x: Tensor) -> tuple[Tensor, Tensor]
    - preactivation = x @ self.weight.T
    - prediction = self.activation_fn(preactivation)
    - return prediction, preactivation
  - extra_repr(self) -> str
    - Return f"in_features={self.weight.shape[1]}, out_features={self.weight.shape[0]}"

IMPORTANT constraints from locked decisions:
- Constructor follows nn.Linear convention: positional (in_features, out_features), keyword activation
- No bias term
- Weight named self.weight (not self.W)
- Xavier uniform initialization with default gain=1.0
- DO NOT wrap forward() in autocast -- that is Phase 3's concern
  </action>
  <verify>
Run these commands:
1. `python -c "from pcn_torch import PCNLayer; layer = PCNLayer(784, 500); print(type(layer).__mro__);"` shows nn.Module in MRO
2. `python -c "from pcn_torch import PCNLayer; import torch; layer = PCNLayer(784, 500, activation='tanh'); x = torch.randn(32, 784); pred, pre = layer(x); print(pred.shape, pre.shape)"` prints torch.Size([32, 500]) twice
3. `python -c "from pcn_torch import get_activation; fn, deriv = get_activation('sigmoid'); print('OK')"` prints OK
4. `ruff check src/pcn_torch/` passes with no errors
5. `ruff format --check src/pcn_torch/` passes
6. `mypy src/pcn_torch/` passes (or passes with only known torch stub issues documented in RESEARCH.md)
  </verify>
  <done>
activations.py contains 4 activation functions (relu, tanh, sigmoid, identity) with hand-coded derivatives and a string-based registry. layers.py contains PCNLayer as an nn.Module with nn.Linear-convention constructor, xavier_uniform_ init, and forward returning (prediction, preactivation). Both modules pass ruff and mypy.
  </done>
</task>

<task type="auto">
  <name>Task 3: Write tests and verify full pipeline</name>
  <files>
    tests/test_activations.py
    tests/test_layers.py
  </files>
  <action>
Write comprehensive tests that verify all Phase 1 success criteria.

**tests/test_activations.py:**
- Import pytest, torch, get_activation from pcn_torch
- Test get_activation returns callable pair for each of: "relu", "tanh", "sigmoid", "identity"
- Test get_activation raises ValueError for unknown name (e.g., "unknown")
- Test relu: fn(tensor_with_negatives) zeros out negatives; deriv returns (x > 0).float()
- Test tanh: fn output in (-1, 1); deriv = 1 - tanh(x)^2, verify with torch.allclose against manual computation
- Test sigmoid: fn output in (0, 1); deriv = s * (1 - s), verify with torch.allclose
- Test identity: fn(x) == x; deriv(x) == ones_like(x)
- Test relu derivative at zero: deriv(torch.tensor([0.0])) == 0.0 (convention from RESEARCH.md)
- All tests use torch.manual_seed(42) for reproducibility where random tensors are needed

**tests/test_layers.py:**
- Import pytest, torch, nn from torch, PCNLayer from pcn_torch
- Test PCNLayer is nn.Module subclass: assert issubclass(PCNLayer, nn.Module)
- Test construction with defaults: PCNLayer(784, 500) -- no error, .weight exists with shape (500, 784)
- Test construction with each activation: PCNLayer(10, 5, activation='tanh'), PCNLayer(10, 5, activation='sigmoid'), PCNLayer(10, 5, activation='identity')
- Test invalid activation raises ValueError: PCNLayer(10, 5, activation='nonexistent')
- Test forward pass shapes: input (batch=32, in=784), output (prediction, preactivation) both (32, 500)
- Test forward pass with different batch sizes (1, 16, 128)
- Test weight is nn.Parameter and is in list(layer.parameters())
- Test extra_repr contains "in_features" and "out_features"
- Test no bias parameter: assert not any("bias" in name for name, _ in layer.named_parameters())

After writing tests, run the full verification suite:
1. `uv run pytest tests/ -v`
2. `uv run ruff check .`
3. `uv run ruff format --check .`
4. `uv run mypy src/pcn_torch/`

Fix any failures before marking done. If mypy has torch stub issues, add targeted `# type: ignore[...]` comments as documented in RESEARCH.md Pitfall 8.
  </action>
  <verify>
1. `uv run pytest tests/ -v` -- all tests pass (0 failures)
2. `uv run ruff check .` -- no lint errors
3. `uv run ruff format --check .` -- all files formatted
4. `uv run mypy src/pcn_torch/` -- passes (possibly with known torch overrides)
5. `uv run python -c "from pcn_torch import PCNLayer, get_activation, __version__; print(f'pcn_torch {__version__} ready')"` -- prints version
  </verify>
  <done>
All tests pass. Ruff lint and format pass. mypy passes. The package is importable with PCNLayer, get_activation, and __version__. Phase 1 success criteria are met: editable install works, PCNLayer is nn.Module, forward produces correct shapes, all 4 activations with hand-coded derivatives are available, CI config is in place.
  </done>
</task>

</tasks>

<verification>
After all tasks complete, verify Phase 1 success criteria:

1. **Editable install**: `uv run python -c "import pcn_torch; print('import OK')"` succeeds
2. **PCNLayer is nn.Module**: `uv run python -c "from pcn_torch import PCNLayer; from torch import nn; assert issubclass(PCNLayer, nn.Module); print('nn.Module OK')"`
3. **Forward pass shapes**: `uv run python -c "from pcn_torch import PCNLayer; import torch; layer = PCNLayer(784, 500); pred, pre = layer(torch.randn(32, 784)); assert pred.shape == (32, 500); assert pre.shape == (32, 500); print('shapes OK')"`
4. **All activations available**: `uv run python -c "from pcn_torch import get_activation; [get_activation(n) for n in ('relu', 'tanh', 'sigmoid', 'identity')]; print('activations OK')"`
5. **Tests pass**: `uv run pytest tests/ -v` -- 0 failures
6. **Lint pass**: `uv run ruff check . && uv run ruff format --check .`
7. **Type check pass**: `uv run mypy src/pcn_torch/`
</verification>

<success_criteria>
- `pip install -e .` (or `uv sync --dev`) installs pcn-torch without errors
- `from pcn_torch import PCNLayer` succeeds; PCNLayer is nn.Module subclass
- PCNLayer(784, 500, activation='relu').forward(torch.randn(32, 784)) returns two tensors of shape (32, 500)
- get_activation works for relu, tanh, sigmoid, identity -- each with hand-coded derivative
- pytest, ruff, and mypy all pass
- CI workflow file exists for Python 3.10-3.13 matrix
</success_criteria>

<output>
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md`
</output>
