---
phase: 04-integration-publishing
plan: 04
type: execute
wave: 2
depends_on:
  - 04-01
  - 04-02
  - 04-03
files_modified:
  - dist/pcn_torch-1.0.0-py3-none-any.whl
  - dist/pcn_torch-1.0.0.tar.gz
autonomous: false

user_setup:
  - service: testpypi
    why: "Verify the package uploads and installs correctly before the real PyPI publish"
    env_vars:
      - name: TESTPYPI_TOKEN
        source: "test.pypi.org → Account Settings → API tokens → Add API token (scope: Entire account)"
    account_setup:
      - "Create account at https://test.pypi.org if you don't have one"
    dashboard_config:
      - task: "Generate API token"
        location: "test.pypi.org → Account Settings → API tokens"

must_haves:
  truths:
    - "git tag v1.0.0 exists (hatch-vcs reads this to stamp the version)"
    - "uv build produces dist/pcn_torch-1.0.0-py3-none-any.whl (version 1.0.0, not 0.0.0.dev)"
    - "uvx twine check dist/* passes (README renders correctly on PyPI)"
    - "package uploaded to TestPyPI without error"
    - "pip install from TestPyPI installs pcn-torch 1.0.0 and import pcn_torch; print(__version__) outputs 1.0.0"
  artifacts:
    - path: "dist/pcn_torch-1.0.0-py3-none-any.whl"
      provides: "Installable wheel stamped with v1.0.0"
    - path: "dist/pcn_torch-1.0.0.tar.gz"
      provides: "Source distribution for PyPI"
  key_links:
    - from: "git tag v1.0.0"
      to: "uv build version stamp"
      via: "hatch-vcs reads git tag before build"
      pattern: "v1.0.0"
    - from: "dist/pcn_torch-1.0.0-py3-none-any.whl"
      to: "TestPyPI"
      via: "uv publish --publish-url https://test.pypi.org/legacy/"
      pattern: "test.pypi.org"
---

<objective>
Tag v1.0.0, build the wheel + sdist, verify the package artifacts, publish to TestPyPI, and verify the install. Real PyPI publish is left as a manual step.

Purpose: Validates the complete release pipeline (tagging, building, checking, uploading) so the user can publish to real PyPI with confidence by running one command.

Output: dist/pcn_torch-1.0.0-py3-none-any.whl and dist/pcn_torch-1.0.0.tar.gz, uploaded to TestPyPI and verified installable.
</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/04-integration-publishing/04-CONTEXT.md
@.planning/phases/04-integration-publishing/04-RESEARCH.md
@.planning/phases/04-integration-publishing/04-01-SUMMARY.md
@.planning/phases/04-integration-publishing/04-02-SUMMARY.md
@.planning/phases/04-integration-publishing/04-03-SUMMARY.md
@pyproject.toml
</context>

<tasks>

<task type="auto">
  <name>Task 1: Tag v1.0.0 and build distribution artifacts</name>
  <files>dist/pcn_torch-1.0.0-py3-none-any.whl, dist/pcn_torch-1.0.0.tar.gz</files>
  <action>
Run these steps in order. The tag MUST precede the build — hatch-vcs reads the git tag at build time to stamp the version.

**Step 1: Verify preconditions (plans 04-01, 04-02, 04-03 are complete)**

```bash
# Confirm torch is not in production deps
grep '"torch' pyproject.toml
# Expected: no match (or match only in a comment)

# Confirm authors are set
grep "Enrique Mendez" pyproject.toml
# Expected: author line

# Confirm examples/ and README.md exist
ls examples/cifar10.py README.md
# Expected: both files listed
```

**Step 2: Create the v1.0.0 git tag**

```bash
# Check no v1.0.0 tag exists yet
git tag --list | grep "v1.0.0"
# Expected: no output

# Create annotated tag
git tag -a v1.0.0 -m "Release v1.0.0"

# Verify tag exists
git tag --list | grep "v1.0.0"
# Expected: v1.0.0
```

**Step 3: Build distribution artifacts**

```bash
uv build
```

This produces two files in dist/:
- `pcn_torch-1.0.0-py3-none-any.whl`
- `pcn_torch-1.0.0.tar.gz`

**Step 4: Verify version is stamped correctly**

```bash
ls dist/
# Expected: shows pcn_torch-1.0.0-py3-none-any.whl and pcn_torch-1.0.0.tar.gz
# If filenames contain ".dev" or "+g" hash: STOP — tag was created after a prior build attempt.
# Fix: delete dist/, ensure tag exists (git tag --list), re-run uv build.
```

**Step 5: Validate artifacts with twine check**

```bash
uvx twine check dist/*
```

Expected output:
```
Checking dist/pcn_torch-1.0.0-py3-none-any.whl: PASSED
Checking dist/pcn_torch-1.0.0.tar.gz: PASSED
```

If twine check reports FAILED on description: the README has a Markdown issue. Fix README.md and rebuild.
  </action>
  <verify>
```bash
# Verify wheel is stamped 1.0.0 (not dev version)
ls dist/ | grep "1.0.0"
# Expected: pcn_torch-1.0.0-py3-none-any.whl and pcn_torch-1.0.0.tar.gz

# Verify no .dev suffix in filenames
ls dist/ | grep ".dev"
# Expected: no output

# Verify twine check passes
uvx twine check dist/*
# Expected: PASSED for both wheel and sdist

# Verify git tag exists
git tag --list | grep "v1.0.0"
# Expected: v1.0.0
```
  </verify>
  <done>
- git tag v1.0.0 created
- dist/pcn_torch-1.0.0-py3-none-any.whl exists (1.0.0, no dev suffix)
- dist/pcn_torch-1.0.0.tar.gz exists
- uvx twine check dist/* passes for both artifacts
  </done>
</task>

<task type="checkpoint:human-action" gate="blocking">
  <what-built>
Task 1 built and verified the distribution artifacts:
- dist/pcn_torch-1.0.0-py3-none-any.whl
- dist/pcn_torch-1.0.0.tar.gz
- Both pass `uvx twine check`

To publish to TestPyPI, an API token is required. Claude cannot create TestPyPI accounts or generate tokens.
  </what-built>
  <instructions>
1. Go to https://test.pypi.org (create a free account if you don't have one)
2. Navigate to: Account Settings → API tokens → Add API token
3. Name: "pcn-torch-testpypi" (or any name)
4. Scope: Entire account (or "Project: pcn-torch" if the project already exists on TestPyPI)
5. Click "Add token"
6. Copy the token (starts with "pypi-")
7. Set it as an environment variable in your shell:
   ```
   $env:TESTPYPI_TOKEN = "pypi-..."   # PowerShell (Windows)
   # OR
   export TESTPYPI_TOKEN="pypi-..."   # bash
   ```

Alternatively, if you want to SKIP TestPyPI and go directly to real PyPI:
- Get a token from https://pypi.org (same steps)
- Set $env:PYPI_TOKEN = "pypi-..."
- Type "skip-testpypi" when resuming

If you want to skip publishing entirely for now:
- Type "skip-publish" when resuming
  </instructions>
  <resume-signal>Type "token-set" when TESTPYPI_TOKEN is in your environment, "skip-testpypi" to publish to real PyPI, or "skip-publish" to skip publishing entirely</resume-signal>
</task>

<task type="auto">
  <name>Task 2: Publish to TestPyPI and verify install</name>
  <files></files>
  <action>
**This task runs only if the user typed "token-set" at the checkpoint. If they typed "skip-testpypi" or "skip-publish", document that in the summary and skip this task.**

**If token-set:**

**Step 1: Publish to TestPyPI**

```bash
uv publish \
  --publish-url https://test.pypi.org/legacy/ \
  --token $TESTPYPI_TOKEN \
  dist/pcn_torch-1.0.0-py3-none-any.whl \
  dist/pcn_torch-1.0.0.tar.gz
```

Expected output: "Uploading pcn_torch-1.0.0-py3-none-any.whl" and "Uploading pcn_torch-1.0.0.tar.gz", both with success messages.

If you get "File already exists": The package was already uploaded to TestPyPI with this version. This is fine — it means the upload works.

If you get "403 Forbidden": The token may be scoped to a different project. Create a new token with "Entire account" scope.

**Step 2: Wait ~30 seconds for TestPyPI to index the package**

```bash
# Wait for package to be available
sleep 30
```

**Step 3: Verify install from TestPyPI in an isolated environment**

```bash
# Create a fresh temporary venv for verification
uv venv /tmp/pcn-verify --python 3.11

# Install from TestPyPI (torch must already exist in the base environment or be available)
# Use --extra-index-url to get torch from real PyPI (torch is peer dep, not on TestPyPI)
uv pip install \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  pcn-torch \
  --python /tmp/pcn-verify/Scripts/python.exe
```

If the above fails due to torch not being on TestPyPI (expected), install torch first:
```bash
uv pip install torch --python /tmp/pcn-verify/Scripts/python.exe
uv pip install pcn-torch \
  --index-url https://test.pypi.org/simple/ \
  --extra-index-url https://pypi.org/simple/ \
  --python /tmp/pcn-verify/Scripts/python.exe
```

**Step 4: Verify version**

```bash
/tmp/pcn-verify/Scripts/python.exe -c "import pcn_torch; print(pcn_torch.__version__)"
# Expected: 1.0.0
```

**Step 5: Confirm real PyPI publish instructions**

Print clearly to the user what to run when they're ready to publish to real PyPI:

```
=========================================================
TestPyPI verification complete. Package works correctly.

To publish to REAL PyPI (do this manually when ready):
  uv publish --token $PYPI_TOKEN

Get your PyPI token from: https://pypi.org
=========================================================
```
  </action>
  <verify>
```bash
# Verify the package is findable on TestPyPI
# (visit https://test.pypi.org/project/pcn-torch/ in browser, or use API)
uv run python -c "
import urllib.request, json
url = 'https://test.pypi.org/pypi/pcn-torch/json'
try:
    with urllib.request.urlopen(url, timeout=10) as r:
        data = json.loads(r.read())
    version = data['info']['version']
    print(f'TestPyPI shows pcn-torch version: {version}')
except Exception as e:
    print(f'Could not verify TestPyPI listing: {e}')
"
# Expected: TestPyPI shows pcn-torch version: 1.0.0
```
  </verify>
  <done>
- Package uploaded to TestPyPI without errors (or skip documented if user chose to skip)
- Import from TestPyPI install returns `__version__ == "1.0.0"`
- Real PyPI publish instructions printed clearly for user to run manually
  </done>
</task>

</tasks>

<verification>
```bash
# Comprehensive final checks

# 1. Tag exists
git tag --list | grep "v1.0.0"
# Expected: v1.0.0

# 2. Artifacts are version-stamped correctly
ls dist/ | grep "1.0.0"
# Expected: both wheel and sdist at 1.0.0

# 3. wheel does NOT require torch
uv run python -c "
import zipfile, json
with zipfile.ZipFile('dist/pcn_torch-1.0.0-py3-none-any.whl') as z:
    for name in z.namelist():
        if 'METADATA' in name and 'dist-info' in name:
            meta = z.read(name).decode()
            lines = [l for l in meta.split('\n') if l.startswith('Requires-Dist')]
            print('Requires-Dist lines:', lines)
"
# Expected: Requires-Dist: numpy and Requires-Dist: rich — NOT torch

# 4. Verify examples/ is excluded from wheel
uv run python -c "
import zipfile
with zipfile.ZipFile('dist/pcn_torch-1.0.0-py3-none-any.whl') as z:
    examples = [n for n in z.namelist() if 'example' in n.lower()]
    print('examples in wheel:', examples)
"
# Expected: [] (empty — examples/ excluded from wheel)
```
</verification>

<success_criteria>
- git tag v1.0.0 created on the current commit
- dist/pcn_torch-1.0.0-py3-none-any.whl exists (no .dev suffix)
- dist/pcn_torch-1.0.0.tar.gz exists
- uvx twine check dist/* shows PASSED for both
- Wheel's Requires-Dist does NOT include torch
- Wheel does NOT include examples/ directory
- Either: package verified on TestPyPI, OR user chose to skip with documented reason
- User has clear instructions for real PyPI publish (one command)
</success_criteria>

<output>
After completion, create `.planning/phases/04-integration-publishing/04-04-SUMMARY.md`
</output>
