diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..325b198 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +# Kerf Engine — Docker build context exclusions +.git +.gsd +.venv +venv +__pycache__ +*.pyc +*.pyo +.pytest_cache +*.egg-info +.env +.env.* +node_modules +.next +dist +build +coverage +.cache +tmp +*.log +.DS_Store +Thumbs.db diff --git a/.gsd/event-log.jsonl b/.gsd/event-log.jsonl index cd93889..4c7b991 100644 --- a/.gsd/event-log.jsonl +++ b/.gsd/event-log.jsonl @@ -9,3 +9,4 @@ {"cmd":"complete-task","params":{"milestoneId":"M001","sliceId":"S02","taskId":"T03"},"ts":"2026-03-26T04:39:50.468Z","actor":"agent","hash":"00412cfd0b09e3c4","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} {"cmd":"complete-slice","params":{"milestoneId":"M001","sliceId":"S02"},"ts":"2026-03-26T04:41:58.014Z","actor":"agent","hash":"296c12d4a2f536c8","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} {"cmd":"complete-task","params":{"milestoneId":"M001","sliceId":"S03","taskId":"T01"},"ts":"2026-03-26T04:45:48.732Z","actor":"agent","hash":"a1c0c74b1d7c5d15","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} +{"cmd":"complete-task","params":{"milestoneId":"M001","sliceId":"S03","taskId":"T02"},"ts":"2026-03-26T04:49:33.566Z","actor":"agent","hash":"fc8b517936769f11","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} diff --git a/.gsd/milestones/M001/slices/S03/S03-PLAN.md b/.gsd/milestones/M001/slices/S03/S03-PLAN.md index df7af4a..f9d13c1 100644 --- a/.gsd/milestones/M001/slices/S03/S03-PLAN.md +++ b/.gsd/milestones/M001/slices/S03/S03-PLAN.md @@ -14,7 +14,7 @@ - Estimate: 45min - Files: engine/presets/sign.json, engine/presets/patch.json, engine/presets/stencil.json, engine/presets/detailed.json, engine/presets/custom.json, engine/presets/loader.py, engine/api/routes.py - Verify: cd engine && python -m pytest tests/ -v -k preset -- [ ] **T02: Engine Dockerfile + healthcheck** — 1. Create docker/Dockerfile.engine (multi-stage build) +- [x] **T02: Created multi-stage Dockerfile.engine with healthcheck endpoint; container builds, starts, and responds healthy on /engine/health** — 1. Create docker/Dockerfile.engine (multi-stage build) 2. Install system deps: libopencv, potrace libs 3. Install Python deps from pyproject.toml 4. Add healthcheck endpoint: GET /engine/health → {status: 'ok'} diff --git a/.gsd/milestones/M001/slices/S03/tasks/T01-VERIFY.json b/.gsd/milestones/M001/slices/S03/tasks/T01-VERIFY.json new file mode 100644 index 0000000..8da7af8 --- /dev/null +++ b/.gsd/milestones/M001/slices/S03/tasks/T01-VERIFY.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "taskId": "T01", + "unitId": "M001/S03/T01", + "timestamp": 1774500352182, + "passed": false, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd engine", + "exitCode": 0, + "durationMs": 4, + "verdict": "pass" + }, + { + "command": "python -m pytest tests/ -v -k preset", + "exitCode": 127, + "durationMs": 3, + "verdict": "fail" + } + ], + "retryAttempt": 1, + "maxRetries": 2 +} diff --git a/.gsd/milestones/M001/slices/S03/tasks/T02-SUMMARY.md b/.gsd/milestones/M001/slices/S03/tasks/T02-SUMMARY.md new file mode 100644 index 0000000..8420208 --- /dev/null +++ b/.gsd/milestones/M001/slices/S03/tasks/T02-SUMMARY.md @@ -0,0 +1,84 @@ +--- +id: T02 +parent: S03 +milestone: M001 +provides: [] +requires: [] +affects: [] +key_files: ["docker/Dockerfile.engine", "engine/api/routes.py", ".dockerignore"] +key_decisions: ["Multi-stage build separates build deps (build-essential, potrace-dev, agg-dev) from runtime (libpotrace0, libagg2, curl)", "Health endpoint at /engine/health for namespace consistency with other /engine/* routes", "Engine image contains only engine source (api, pipeline, output, presets, main.py) — no App dependencies"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "Built image successfully (exit 0), ran container on port 8100, confirmed /engine/health returns {"status":"ok"}, Docker health status shows "healthy" with 0 failing streak, presets endpoint works from container, no App files in image, full test suite passes (196/196), preset tests pass (28/28)." +completed_at: 2026-03-26T04:49:33.510Z +blocker_discovered: false +--- + +# T02: Created multi-stage Dockerfile.engine with healthcheck endpoint; container builds, starts, and responds healthy on /engine/health + +> Created multi-stage Dockerfile.engine with healthcheck endpoint; container builds, starts, and responds healthy on /engine/health + +## What Happened +--- +id: T02 +parent: S03 +milestone: M001 +key_files: + - docker/Dockerfile.engine + - engine/api/routes.py + - .dockerignore +key_decisions: + - Multi-stage build separates build deps (build-essential, potrace-dev, agg-dev) from runtime (libpotrace0, libagg2, curl) + - Health endpoint at /engine/health for namespace consistency with other /engine/* routes + - Engine image contains only engine source (api, pipeline, output, presets, main.py) — no App dependencies +duration: "" +verification_result: passed +completed_at: 2026-03-26T04:49:33.527Z +blocker_discovered: false +--- + +# T02: Created multi-stage Dockerfile.engine with healthcheck endpoint; container builds, starts, and responds healthy on /engine/health + +**Created multi-stage Dockerfile.engine with healthcheck endpoint; container builds, starts, and responds healthy on /engine/health** + +## What Happened + +Created docker/Dockerfile.engine as a two-stage build: builder stage installs build-essential, libagg-dev, and libpotrace-dev to compile pypotrace, then installs all Python deps into /opt/venv. Runtime stage uses python:3.11-slim with only runtime libs (libpotrace0, libagg2, curl), copies the venv, and copies only engine source code. Added GET /engine/health endpoint returning {"status": "ok"} to engine/api/routes.py. Configured Docker HEALTHCHECK instruction (15s interval, 5s timeout, 10s start-period, 3 retries). Added .dockerignore for clean builds. Verified container builds, starts, responds healthy, and contains no App/frontend code. + +## Verification + +Built image successfully (exit 0), ran container on port 8100, confirmed /engine/health returns {"status":"ok"}, Docker health status shows "healthy" with 0 failing streak, presets endpoint works from container, no App files in image, full test suite passes (196/196), preset tests pass (28/28). + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `docker build -f docker/Dockerfile.engine -t kerf-engine:dev .` | 0 | ✅ pass | 78700ms | +| 2 | `curl -sf http://localhost:8100/engine/health` | 0 | ✅ pass | 5100ms | +| 3 | `docker inspect kerf-engine-test --format='{{json .State.Health.Status}}'` | 0 | ✅ pass | 200ms | +| 4 | `cd engine && .venv/bin/python -m pytest tests/ -v -k preset` | 0 | ✅ pass | 600ms | +| 5 | `cd engine && .venv/bin/python -m pytest tests/ -v` | 0 | ✅ pass | 940ms | + + +## Deviations + +Added /engine/health on the router for namespace consistency (root /health already existed in main.py). Both endpoints now coexist. + +## Known Issues + +None. + +## Files Created/Modified + +- `docker/Dockerfile.engine` +- `engine/api/routes.py` +- `.dockerignore` + + +## Deviations +Added /engine/health on the router for namespace consistency (root /health already existed in main.py). Both endpoints now coexist. + +## Known Issues +None. diff --git a/.gsd/state-manifest.json b/.gsd/state-manifest.json index 87f1cba..34a5506 100644 --- a/.gsd/state-manifest.json +++ b/.gsd/state-manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "exported_at": "2026-03-26T04:45:48.731Z", + "exported_at": "2026-03-26T04:49:33.565Z", "milestones": [ { "id": "M001", @@ -774,19 +774,27 @@ "milestone_id": "M001", "slice_id": "S03", "id": "T02", - "title": "Engine Dockerfile + healthcheck", - "status": "pending", - "one_liner": "", - "narrative": "", - "verification_result": "", + "title": "Created multi-stage Dockerfile.engine with healthcheck endpoint; container builds, starts, and responds healthy on /engine/health", + "status": "complete", + "one_liner": "Created multi-stage Dockerfile.engine with healthcheck endpoint; container builds, starts, and responds healthy on /engine/health", + "narrative": "Created docker/Dockerfile.engine as a two-stage build: builder stage installs build-essential, libagg-dev, and libpotrace-dev to compile pypotrace, then installs all Python deps into /opt/venv. Runtime stage uses python:3.11-slim with only runtime libs (libpotrace0, libagg2, curl), copies the venv, and copies only engine source code. Added GET /engine/health endpoint returning {\"status\": \"ok\"} to engine/api/routes.py. Configured Docker HEALTHCHECK instruction (15s interval, 5s timeout, 10s start-period, 3 retries). Added .dockerignore for clean builds. Verified container builds, starts, responds healthy, and contains no App/frontend code.", + "verification_result": "Built image successfully (exit 0), ran container on port 8100, confirmed /engine/health returns {\"status\":\"ok\"}, Docker health status shows \"healthy\" with 0 failing streak, presets endpoint works from container, no App files in image, full test suite passes (196/196), preset tests pass (28/28).", "duration": "", - "completed_at": null, + "completed_at": "2026-03-26T04:49:33.510Z", "blocker_discovered": false, - "deviations": "", - "known_issues": "", - "key_files": [], - "key_decisions": [], - "full_summary_md": "", + "deviations": "Added /engine/health on the router for namespace consistency (root /health already existed in main.py). Both endpoints now coexist.", + "known_issues": "None.", + "key_files": [ + "docker/Dockerfile.engine", + "engine/api/routes.py", + ".dockerignore" + ], + "key_decisions": [ + "Multi-stage build separates build deps (build-essential, potrace-dev, agg-dev) from runtime (libpotrace0, libagg2, curl)", + "Health endpoint at /engine/health for namespace consistency with other /engine/* routes", + "Engine image contains only engine source (api, pipeline, output, presets, main.py) — no App dependencies" + ], + "full_summary_md": "---\nid: T02\nparent: S03\nmilestone: M001\nkey_files:\n - docker/Dockerfile.engine\n - engine/api/routes.py\n - .dockerignore\nkey_decisions:\n - Multi-stage build separates build deps (build-essential, potrace-dev, agg-dev) from runtime (libpotrace0, libagg2, curl)\n - Health endpoint at /engine/health for namespace consistency with other /engine/* routes\n - Engine image contains only engine source (api, pipeline, output, presets, main.py) — no App dependencies\nduration: \"\"\nverification_result: passed\ncompleted_at: 2026-03-26T04:49:33.527Z\nblocker_discovered: false\n---\n\n# T02: Created multi-stage Dockerfile.engine with healthcheck endpoint; container builds, starts, and responds healthy on /engine/health\n\n**Created multi-stage Dockerfile.engine with healthcheck endpoint; container builds, starts, and responds healthy on /engine/health**\n\n## What Happened\n\nCreated docker/Dockerfile.engine as a two-stage build: builder stage installs build-essential, libagg-dev, and libpotrace-dev to compile pypotrace, then installs all Python deps into /opt/venv. Runtime stage uses python:3.11-slim with only runtime libs (libpotrace0, libagg2, curl), copies the venv, and copies only engine source code. Added GET /engine/health endpoint returning {\"status\": \"ok\"} to engine/api/routes.py. Configured Docker HEALTHCHECK instruction (15s interval, 5s timeout, 10s start-period, 3 retries). Added .dockerignore for clean builds. Verified container builds, starts, responds healthy, and contains no App/frontend code.\n\n## Verification\n\nBuilt image successfully (exit 0), ran container on port 8100, confirmed /engine/health returns {\"status\":\"ok\"}, Docker health status shows \"healthy\" with 0 failing streak, presets endpoint works from container, no App files in image, full test suite passes (196/196), preset tests pass (28/28).\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `docker build -f docker/Dockerfile.engine -t kerf-engine:dev .` | 0 | ✅ pass | 78700ms |\n| 2 | `curl -sf http://localhost:8100/engine/health` | 0 | ✅ pass | 5100ms |\n| 3 | `docker inspect kerf-engine-test --format='{{json .State.Health.Status}}'` | 0 | ✅ pass | 200ms |\n| 4 | `cd engine && .venv/bin/python -m pytest tests/ -v -k preset` | 0 | ✅ pass | 600ms |\n| 5 | `cd engine && .venv/bin/python -m pytest tests/ -v` | 0 | ✅ pass | 940ms |\n\n\n## Deviations\n\nAdded /engine/health on the router for namespace consistency (root /health already existed in main.py). Both endpoints now coexist.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `docker/Dockerfile.engine`\n- `engine/api/routes.py`\n- `.dockerignore`\n", "description": "1. Create docker/Dockerfile.engine (multi-stage build)\n2. Install system deps: libopencv, potrace libs\n3. Install Python deps from pyproject.toml\n4. Add healthcheck endpoint: GET /engine/health → {status: 'ok'}\n5. Configure Dockerfile HEALTHCHECK instruction\n6. Build and test container starts\n7. Verify API responds from inside container\n8. Ensure no App dependencies in engine image", "estimate": "30min", "files": [ @@ -942,6 +950,61 @@ "verdict": "✅ pass", "duration_ms": 960, "created_at": "2026-03-26T04:45:48.689Z" + }, + { + "id": 10, + "task_id": "T02", + "slice_id": "S03", + "milestone_id": "M001", + "command": "docker build -f docker/Dockerfile.engine -t kerf-engine:dev .", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 78700, + "created_at": "2026-03-26T04:49:33.510Z" + }, + { + "id": 11, + "task_id": "T02", + "slice_id": "S03", + "milestone_id": "M001", + "command": "curl -sf http://localhost:8100/engine/health", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 5100, + "created_at": "2026-03-26T04:49:33.510Z" + }, + { + "id": 12, + "task_id": "T02", + "slice_id": "S03", + "milestone_id": "M001", + "command": "docker inspect kerf-engine-test --format='{{json .State.Health.Status}}'", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 200, + "created_at": "2026-03-26T04:49:33.510Z" + }, + { + "id": 13, + "task_id": "T02", + "slice_id": "S03", + "milestone_id": "M001", + "command": "cd engine && .venv/bin/python -m pytest tests/ -v -k preset", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 600, + "created_at": "2026-03-26T04:49:33.510Z" + }, + { + "id": 14, + "task_id": "T02", + "slice_id": "S03", + "milestone_id": "M001", + "command": "cd engine && .venv/bin/python -m pytest tests/ -v", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 940, + "created_at": "2026-03-26T04:49:33.510Z" } ] } \ No newline at end of file diff --git a/docker/Dockerfile.engine b/docker/Dockerfile.engine new file mode 100644 index 0000000..390bd86 --- /dev/null +++ b/docker/Dockerfile.engine @@ -0,0 +1,61 @@ +# ── Kerf Engine — multi-stage Docker build ── +# Standalone raster-to-vector conversion API (no App dependencies) + +# ── Stage 1: Build dependencies ── +FROM python:3.11-slim AS builder + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + pkg-config \ + libagg-dev \ + libpotrace-dev \ + libpotrace0 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +# Install Python deps into a virtual-env we can copy later +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +COPY engine/pyproject.toml . +RUN pip install --no-cache-dir . 2>/dev/null || true +# The above may fail because package source isn't present yet — install deps directly +RUN pip install --no-cache-dir \ + "fastapi>=0.110" \ + "uvicorn[standard]>=0.29" \ + "opencv-python-headless>=4.9" \ + "pypotrace>=0.3" \ + "vtracer>=0.6" \ + "python-multipart>=0.0.9" \ + "Pillow>=10.2" \ + "ezdxf>=1.0" + +# ── Stage 2: Runtime image ── +FROM python:3.11-slim AS runtime + +# Runtime-only system libs for pypotrace +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpotrace0 \ + libagg2 \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# Copy the virtual-env from the builder +COPY --from=builder /opt/venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" + +# Copy engine source only — no App code +WORKDIR /app +COPY engine/main.py . +COPY engine/api/ ./api/ +COPY engine/pipeline/ ./pipeline/ +COPY engine/output/ ./output/ +COPY engine/presets/ ./presets/ + +EXPOSE 8000 + +HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \ + CMD curl -sf http://localhost:8000/engine/health || exit 1 + +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/engine/api/routes.py b/engine/api/routes.py index 3a48445..3ca93dc 100644 --- a/engine/api/routes.py +++ b/engine/api/routes.py @@ -14,6 +14,13 @@ from presets.loader import all_presets, preset_names, resolve_params router = APIRouter() + +@router.get("/engine/health") +async def health(): + """Healthcheck endpoint for container orchestration.""" + return {"status": "ok"} + + VALID_MODES = {"potrace", "vtracer"} VALID_OUTPUT_FORMATS = {"svg", "dxf", "json"}