The v0.8 milestone: installable on any Debian-based server via docker
(Gitea registry image, honest sandbox degrade), deb (amd64+arm64,
advisory postinst), and arch-aware binary — with release consistency
enforced by pre-commit hooks + Gitea Actions (3-runner fleet on CT 113,
machine-made releases with the full 8-asset matrix + registry image), the
CLI carrying the full suite (guide, channel-aware doctor), and the admin
runbook (deploy/README.md, evidence-backed recipes).
---ci---
phase: 6
milestone: v0.8
status: complete
requirements:
covered: [REQ-8-001, REQ-8-002, REQ-8-003, REQ-8-004, REQ-8-005]
partial: []
---/ci---
The domain root (nextcraft.coreci.dev) served the API's JSON 404 for three
releases. Root cause (probe-verified on the host): the deployed uvicorn ran
without AI_WEB_STATIC_DIR because apps/web/out was never built — dev.ts
auto-wiring silently skips when the export is absent, and NOTHING detects
the mount-less boot. HAProxy was never the problem; the JSON 404 at / IS
the mount-less signature (pinned by test_web_static.py since v0.3.6).
Class fixes (the deploy experience, per founder directive):
- toolchain discovery (lib/toolchain.ts): node/pnpm behind nvm,
PNPM_HOME, or corepack are FOUND even when non-interactive shells
can't see them — the host had pnpm the whole time; doctor, bootstrap
and deploy all resolve through it (PATH → nvm → PNPM_HOME → corepack).
- nextcraft deploy: the one command the installer hands off to —
clone-or-update → pnpm install → venv → web export build → restart →
verify. Fails LOUDLY if the site isn't actually served. Idempotent:
same command for first install and upgrades. deploy --service installs
a generated systemd unit (real paths, Restart=always, enabled) so the
stack survives reboots; systemd becomes the supervisor (dev -d and
stop respect it).
- create_app(): a mount-less boot now WARNs naming the fix (G-11
doctrine — the failure was silent for three releases).
- verify: web-export presence check + LIVE MOUNT PROBE — when :8420 is
serving, GET / must return HTML, not the API's JSON 404. This is the
check that would have caught every prior outage.
- install.sh next-steps → `nextcraft deploy --service`; README +
deploy/README rewritten around the one-command flow.
478 backend tests (+1 loud-boot pin); CLI 59 (45→59: toolchain discovery
x7, deploy x4, verify probes x3, hermeticity fixes); web 14/14;
typecheck 8/8; ruff clean; static export builds.
---ci---
phase: hotfix
milestone: v0.5
status: complete
type: hotfix
---/ci---
P0-C1: mock identity ids were hash(learner, DOB)-deterministic — resubmit
after a TERMINAL verdict collided on the insert-only PK → unhandled 500,
permanently blocking re-verification (the D1 lockout one path deeper).
Ids now mint uniquely per submit (nonce + counter, PII-free); verdict
determinism preserved in poll(). Regression: resubmit-after-reject → 200
fresh pending → G-13 409.
P0-C2: defense audio answers 500'd on every default deployment — the mock
provider's empty transcript queue raised MockVoiceFailure (unhandled) the
first time a learner used the mic; same for a real endpoint outage.
transcribe() now maps provider RuntimeErrors to honest 502s (house
pattern); typed answers unaffected. Regression: unscripted mock → 502,
then typed answer → 200 on the same defense.
P0-C3: v0.5's variant_record columns brick a pre-v0.5 DB (create_all
never ALTERs) — every variant read/write raised OperationalError after an
in-place upgrade. Idempotent _ensure_v05_columns() backfill in the store
constructor (PRAGMA table_info → ALTER ADD COLUMN with defaults; legacy
rows read as build-kind). Regression: literal v0.4 table → open → read →
write → reopen idempotent. deploy/README documents the in-place note
(fresh-state directive still honored).
477 backend tests (+3); ruff clean; typecheck 8/8.
P1 flags recorded for pre-public hardening (non-blocking): unauthenticated
WS ingest on LAN (grading DoS), browser-mode SR/TTS client gap, defense-id
ms collision, browser-mode audio E2E.
---ci---
phase: 5
milestone: v0.5
status: review
---/ci---
The public site (nextcraft.coreci.dev) 404s because HAProxy forwards the
domain to the ai-service alone and the web app was never reachable (only
:8420 is open). This makes ONE port serve BOTH surfaces:
- Web: `output: 'export'` static build (apps/web/out) with generateStaticParams
for all 7 dynamic routes; NEXT_PUBLIC_AI_SERVICE_URL=self bakes relative
same-origin fetches (engine-base-url "" base; engine-client already
concatenates). No CORS, no mixed content, no Node web server in prod.
- API: optional AI_WEB_STATIC_DIR StaticFiles(html=True) mount AFTER all
routers (API wins; / serves index.html; unknown paths serve the export's
404.html with status 404; set-but-missing dir fails startup loudly).
Default unset — dev/tests unchanged.
- CLI: `dev -d/--detach` (daemon; ~/.nextcraft/run/<clone-hash>/dev.{pid,log},
refuses double-start, auto-wires AI_WEB_STATIC_DIR when out/ exists),
`stop` (SIGTERM→SIGKILL 5s), `log` (-n N lines, -f follow). stdlib only.
Founder directives (this hotfix):
- unattended/daemonized dev with a tail command — dev -d / log / stop
- only 8420 reachable — single-port same-origin topology
- runtime state must NOT live in the repo — db_path default moves to
~/.nextcraft/data/nextcraft.db, sandbox_dir to ~/.nextcraft/sandboxes/;
expanduser validator (AI_DB_PATH=~/... works); .env.example stops pinning
repo paths; daemon pid/log also under ~/.nextcraft.
Test hygiene: conftest settings fixture pins stores to tmp_path (was silently
using repo defaults); sandbox tests use a bind-mount-safe sandbox_dir fixture
(overlayfs /tmp breaks userns bind mounts with ENODEV — home ext4 works).
Gates: typecheck 8/8; pnpm build emits out/ (all 73 pages static); web tests
5/5; cli tests green incl. real daemon lifecycle; backend 413 passed
(+4 static-mount tests); ruff clean.
---ci---
phase: hotfix
milestone: v0.4
status: complete
type: hotfix
---/ci---