Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| baa9ef5d2d |
@@ -8,6 +8,21 @@ Nextcraft is a TypeScript monorepo (pnpm workspaces + turborepo) with a Next.js
|
||||
|
||||
**v0.4 additions (Distribution & Bootstrap CLI, founder directive D-016):** a new `apps/cli` package — the `nextcraft` bootstrap CLI (`doctor`/`bootstrap`/`verify`/`dev`) compiled to a self-contained linux x64 binary via **Node SEA** (probe-verified: Go/Rust absent, node v24.15.0 SEA-capable), installed by a repo-served one-liner script that resolves the latest Gitea release, downloads binary + sha256 sidecar, verifies, and installs to `~/.local/bin`. Every release from v0.4 onward attaches the binary + checksum as release assets (the "ongoing binaries" requirement). The CLI is a thin wrapper: all orchestration logic stays in `apps/ai-service/scripts/` (bootstrap.sh/dev.sh) — the CLI composes them via subprocess (A-202), duplicating nothing. Previously-planned v0.4 seams (real STT/TTS, KYC, design/sim envs, seq-lease) move to v0.5.
|
||||
|
||||
### v0.6 Research Conclusions (Phase 0 RESEARCH)
|
||||
|
||||
32. **D-048 Marketplace module layout mirrors D-031** — one new engine module `ai_service/marketplace/` inside apps/ai-service (no new apps, no new ports): `base.py` (pydantic contracts: `Posting`, `Employer`, `PostingTags`, `IngestPosting`), `store.py` (6th D-027 store family, D-049), `ingest.py` (PostingSource adapters, D-050), `tagging.py` (D-052), `embedding/` (protocol + mock + openai_compat + factory, D-051), `search.py` (hybrid facet+cosine ranking, A-405), `enrichment.py` (D-053), `resume.py` (D-054), with the HTTP surface in `ai_service/api/marketplace.py` (`/v1/marketplace/*` router) + `api/marketplace_admin.py` (`/v1/marketplace/admin/*`, D-057) composed via `deps.py` app.state DI (`get_marketplace_store`, `get_embedding_provider`). Boundary rules identical to identity/voice: `marketplace/` never imports `agents/` or `api/`; settings in `config.py` (backend-engineer territory); the G-18 stub MOVES from `api/identity.py` into the new router and becomes real (D-058) — `identity/` keeps only the `require_verified_adult` gate dependency it exports today.
|
||||
33. **D-049 Sixth store = one SQLite DB, five tables, DefenseStore conventions** — the marketplace store joins the shared `~/.nextcraft/data/nextcraft.db` (all 5 prior stores already share it; `Settings().db_path`). Tables: `posting` (id PK, source, external_id, employer_id, title, description, skills/seniority/category/location/remote/salary JSON+portable columns, status `draft|published|closed`, posted_at, unique constraint on (source, external_id) for idempotent upsert per A-402); `employer` (id PK, ingested raw fields, unique per source); `embedding` (posting_id PK, `vector` BLOB of struct-packed little-endian float32, `model`, `dim` — A-403/A-404 per-row provenance so lazy re-embed on provider change works); `application` (id PK, posting_id, learner_id, status `submitted`, created_at, unique (posting_id, learner_id) for A-408 idempotent apply); `employer_enrichment` (append-only: id PK, employer_id, field, value, source `llm|manual`, model, created_at — A-409). All tables mirror the probe-verified identity/store.py mechanics: WAL/synchronous=NORMAL/busy_timeout/foreign_keys pragmas, `sa_type=String/JSON` portable columns, `_validate()` hooks, Protocol + SQLite impl pair, surfaced IntegrityError, tz normalization. `VectorStore` is a protocol method-group on the marketplace store (pgvector-swap-ready, D-019/D-027 pattern).
|
||||
34. **D-050 PostingSource = protocol + two adapters, no scheduler** — `PostingSource` protocol: `fetch() -> Iterable[IngestPosting]` (pydantic, A-402 shape: source, external_id, employer + posting fields). Two v0.6 implementations: `seeded_corpus` (in-repo JSON seed ported ONCE from `packages/mock-data/jobs.ts` (20 jobs) + `employers.ts` (10 employers) per D-021 convention — shared IDs, mock `matchScore` DROPPED since D-051 computes real ones) and `admin_api` (the founder-authored path; the admin router calls ingest directly rather than an HTTP hop). Ingest upserts idempotent on (source, external_id); a `POST /v1/marketplace/admin/ingest` route triggers a source run explicitly (founder-invoked — no cron/crawler, A-401/A-402). The 3M+ external corpus lands later as a third adapter with zero store/route changes.
|
||||
35. **D-051 EmbeddingProvider = D-030 mirror with a corrected real path** — protocol `embed(texts: list[str]) -> list[EmbeddingVector]` (frozen pydantic: `values: list[float]`, `model`, `dim`) in `marketplace/embedding/base.py`. Deterministic mock: 256-dim feature-hash of normalized tokens (pure stdlib, similarity deterministic — tests never call the cloud). Real: `OpenAIEmbedProvider` on the shared httpx pool posting `{"model", "input": [texts]}` to `{AI_EMBED_BASE_URL}/embeddings` (OpenAI-compatible; `_sanitize` key redaction mirroring `openai_audio.py`). **Probe correction to A-404:** ollama-cloud does NOT expose `/v1/embeddings` (404 path-not-found; chat/completions auth-gates 401) — so `AI_EMBED_BASE_URL` must point at a different OpenAI-compatible endpoint (local ollama `:11434/v1` supports it, or any embeddings cloud); provider selection `AI_EMBED_PROVIDER=mock|openai-embed` (default mock, G-11 boot-safe fallback with loud log — exact `voice/factory.py` pattern, `AI_EMBED_MODEL`/`AI_EMBED_API_KEY` env-only). Storage per A-403: struct-packed `<{dim}f` float32 BLOB + model + dim per row; cosine in pure Python (sum of products / norm product, ≤10⁴ postings); candidate side embeds competency-profile text on demand, never stored. Confidence in A-404's parenthetical drops it to ~0.5 but the protocol design is unaffected (endpoint-agnostic by config is the D-014 pattern anyway).
|
||||
36. **D-052 Tagging = D-020 structured_completion, no new agent** — `marketplace/tagging.py` composes the EXISTING `structured_completion(provider, messages, model, schema, schema_hint)` (agents/structured.py, all 4 layers) with a `PostingTags` pydantic schema (skills: list[str] ≤10, seniority enum, category enum) + a versioned prompt constant in `prompts/` (or module-local, matching variants' pattern); no BaseAgent needed — tagging is a one-shot extraction like grading's rubric call. Mock-first for free: `settings.provider` defaults to `mock` and `structured_completion` runs against `MockLLMProvider.chat`, so tests stay cloud-free. Tags persisted as a JSON column on the posting row (D-049); tag facets feed the search filter sidebar (A-405 hard filters); re-tagging on posting update overwrites the tags column (single source of truth, no append-only need — unlike enrichment D-053, tags are derived data).
|
||||
37. **D-053 Enrichment = append-only provenance table + merge-on-read** — per A-409: `employer_enrichment` rows are append-only `(employer_id, field, value, source: llm|manual, model, created_at)`; read precedence **manual > llm > ingested-raw** computed at API-response time (a small `merge_employer()` over rows, raw fields last); every enriched field carries `{value, source}` provenance markers in the API response (A-304 honesty — LLM copy visibly marked). Trigger: a new employer's first published posting fires a one-shot LLM structured extraction (D-020, same layering as D-052, schema: description, social_links, logo_ref) — founder override = upsert a `manual`-source row, never a mutation of `llm` rows. No employer self-serve anywhere (A-407).
|
||||
38. **D-054 Resume parse = pypdf (probe-verified) → D-020 draft extraction** — pypdf 6.19.0 pip-reachable through the existing venv channel (probe 1; the ONLY new dependency, pinned `pypdf>=6.19,<7` in pyproject.toml). Pipeline: upload guard (PDF/TXT/MD only, ≤2 MB, A-410) → text extraction (`pypdf.PdfReader` page-text join for PDFs; raw decode for TXT/MD) → empty-extract on a PDF = honest 422 "text layer required" (no OCR) → cap ~100K chars → D-020 structured extraction into a `ParsedProfile` pydantic schema mapped to the competency-profile shape (competencies, skills, experience bands) → response returns as **DRAFT** (`status: "draft"`) — nothing persists to the learner profile until an explicit confirm call (REQ-6-005 review gate). Raw resume bytes never stored, never logged (A-305 pattern; the existing `_scrub_pii_validation` handler precedent applies). Tests use small generated PDFs or TXT fixtures — cloud-free.
|
||||
39. **D-055 Web migration = client shells + engine-client extension; remove 2 generateStaticParams** — board/detail/employer-profile become one static client shell each fetching at runtime per A-406 (the proven build/defend pattern): `marketplace/page.tsx` keeps its server shell but `JobFilters` receives fetch-backed data (it already owns all filter logic client-side — the facet contract survives unchanged); `jobs/[jobId]` and `employers/[employerId]` drop `generateStaticParams` (the ONLY two removals — learner catalog/learn/build/defend keep theirs, their data stays mock) and render a client shell that fetches then renders (or `notFound()` on 404). `engine-client.ts` gains a marketplace section: `listPostings(filters)`, `getPosting(id)`, `getEmployer(id)`, `searchPostings(query)` → typed against new `packages/types` marketplace wire shapes (data-engineer dual-schema sync); 403 discrimination (verify-CTA vs allowlist) already works. `packages/mock-data` jobs.ts/employers.ts go dormant for the marketplace path (still imported by employer/admin mockups — NOT deleted, A-406/A-407); the seed port (D-050) copies values once. No build-time DB reads — D-039 export build stays repo-only.
|
||||
40. **D-056 SEO = build-time metadata on static set + client JSON-LD on runtime set + ai-service sitemap/robots** — per A-411, split: (a) build-time-static pages (landing, catalog/stack detail, marketplace board, pricing) get `export const metadata` (probe 7: the static-export-compatible API the app already uses in layout.tsx/enroll) + OG tags + server-rendered JSON-LD (`Organization` on employer-adjacent static, `ItemList` on the board) rendered into the export; (b) runtime-data pages (job detail, employer profile) inject `JobPosting`/`Organization` JSON-LD + title client-side after fetch — accepted JS-crawler limitation, recorded; (c) `/sitemap.xml` + `/robots.txt` as plain FastAPI GET routes on the api app enumerating the static page list + all `published` postings — **probe 6 verified mount order: API routes beat the `StaticFiles("/")` mount**, so they win over the export even in single-port production; do NOT use Next `app/sitemap.ts` (it enumerates at build time and cannot see runtime postings under `output: 'export'`). robots.txt points at `/sitemap.xml`; sitemap URLs use the deployed origin (env-configured `AI_PUBLIC_BASE_URL`-style setting, backend-engineer territory).
|
||||
41. **D-057 Admin guard = AI_ADMIN_TOKEN header check, env-only, fail-closed** — all `/v1/marketplace/admin/*` routes (postings create + lifecycle transitions draft→published→closed, ingest trigger, enrichment manual-override, applications review) take a FastAPI dependency `require_admin` comparing `X-Admin-Token` (or `Authorization: Bearer`) against `Settings().admin_token` (`AI_ADMIN_TOKEN`, env-only per D-014, probe: no admin token exists in config today — greenfield). Unset token = admin routes 403-fail-closed (never open; a loud boot warning tells the founder to set it, G-11 doctrine). This is the A-401 operator surface; D-043 learner gate composition is untouched — admin routes do NOT pass the learner allowlist/identity gates (different principal class).
|
||||
42. **D-058 Apply route goes real, composition unchanged** — `POST /v1/marketplace/apply` (moved from api/identity.py into api/marketplace.py, D-048) keeps the exact D-043 order (G-5 allowlist → `require_verified_adult` → rate caps), then: posting must exist and be `published` (404/409 otherwise), insert an `application` row (status `submitted`, unique (posting_id, learner_id) → duplicate apply is idempotent 200-with-existing, A-408), return an honest confirmation payload `{application_id, posting_id, status: "submitted"}` (the `stub/mock` markers die). Founder reviews via `GET /v1/marketplace/admin/applications` (D-057-guarded); no notification infra. Body becomes `{posting_id}` + the learner identity from the verified record rather than a client-asserted `job_id`.
|
||||
43. **D-059 v0.6 wave order = ROADMAP order confirmed with two couplings** — P1 postings backend must ship WITH the web client shells in the same phase (a live store behind mock-reading pages proves nothing — REQ-6-001's success criterion is surfaces reading real data); P2 tagging before P3 search because facets drive the sidebar filters AND the posting text embedded in P3 should include the normalized tags; P4's two workstreams (enrichment REQ-6-004, resume REQ-6-005) are independent of each other — parallelizable within the phase; P5 SEO depends only on P1 (routes exist to enumerate) but stays last-before-final per ROADMAP since sitemap needs `published` postings and the board metadata should reflect final facets; P6 final review + ship. Tagging/enrichment/resume all route through the same D-020 call site — one prompt-review pass covers all three.
|
||||
|
||||
### v0.5 Research Conclusions (Phase 0 RESEARCH)
|
||||
|
||||
25. **D-040 Voice real path = `OpenAIAudioProvider` on the shared httpx pool (D-R01)** — implements the D-030 protocol: `transcribe()` = multipart `POST {AI_VOICE_BASE_URL}/audio/transcriptions` (`file` + `model`, response_format=json → `TranscriptSegment`), `synthesize()` = streaming `POST /audio/speech` (JSON body, raw byte stream, `input` ≤4096 chars). Carries `descriptor = VoiceDescriptor(mode="server", ...)` which defense.py:143 already prefers — mode selection drops in with zero API changes. Constructor takes the lifespan `httpx.AsyncClient` (D-017; read=300s already tolerates multi-minute clips). Factory signature becomes `voice_provider_from_settings(settings, http_client)`. Errors sanitized with key redaction (mirror `llm/openai_compat.py:_sanitize`).
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"phase": 0,
|
||||
"stage": "grill",
|
||||
"milestone": "v0.6",
|
||||
"milestone_name": "marketplace-engine",
|
||||
"phase_role": "pre_execution",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-09-21T22:00:00Z"
|
||||
}
|
||||
@@ -1,3 +1,51 @@
|
||||
# Nextcraft v0.6 — GRILL.md (Adversarial Review Verdict)
|
||||
|
||||
**Stage:** GRILL, Phase 0 pre-execution · **Verdict:** GO-WITH-BINDINGS · **Confidence:** 0.72 · **Date:** 2026-09-21
|
||||
|
||||
## Summary
|
||||
|
||||
The v0.6 Marketplace Engine plan (46 tasks, 6 phases) is well-researched and tightly scoped, but its central P1 web-migration mechanism — removing `generateStaticParams` from the two marketplace dynamic routes — is **provably build-breaking**: Next 15.5.25 (installed `node_modules/next/dist/build/index.js:1244`) hard-throws error E87 for any dynamic route without `generateStaticParams` under `output: 'export'`, and even a function returning `[]` throws (`length > 0` check). The claimed "proven build/defend pattern" precedent does not exist as described — build/defend routes KEPT their `generateStaticParams`. Four quieter gaps (ingest status semantics, pagination-vs-client-filters, learner-profile sidecar persistence, sitemap origin) each ship a broken happy path or an audit flag. All fixed via binding decisions G-101..G-110 + 8 plan edits applied by the orchestrator before execute.
|
||||
|
||||
## Per-Axis Findings
|
||||
|
||||
| Axis | Verdict | Rationale |
|
||||
|------|---------|-----------|
|
||||
| Feasibility | PASS | Personas/territories match proven patterns (voice factory, identity store, D-020 layer); 46 tasks comparable to shipped v0.5. |
|
||||
| Scope discipline | PASS | Pricing/employer-auth/scheduler/crawler all absent (MH-M6, A-407/A-412, D-050); only borderline add is admin posting-edit, defensible as the re-tag trigger within A-401. |
|
||||
| Vertical-slice integrity | FAIL → fixed | P1 backend+shells coupling is right, but the shell mechanism threw E87 at build and runtime-created postings would get no exported page — fixed by G-101 (placeholder param + FastAPI catch-all shell routes). |
|
||||
| Testability | AT-RISK → fixed | MH-1e pinned a build that could not emit; MH-3d's "matchScore exists nowhere" overclaimed (mock-data retains it for employer/admin mockups) — scoped by G-107/G-110. |
|
||||
| Security posture | PASS | Fail-closed admin, D-043 gate order, upload bounds, PII sentinel tests verified against existing code; hardening added by G-105/G-108. |
|
||||
| Performance/capacity | PASS | Brute-force cosine ≤10⁴ honest; SQLite WAL/busy_timeout verified in the identity store; sitemap fine to 50k URLs. |
|
||||
| Operational readiness | AT-RISK → fixed | D-039 deploy fabric intact (mount order verified), pypdf via bootstrap channel sound; founder workflow breaks at ingest-status (G-102) and sitemap-origin (G-105) — now defined. |
|
||||
| Dependency risk | PASS | pypdf verified as the only new dep; JSON-LD/sitemap hand-rolled; embedding mock stdlib-only. A-404's stale "ollama-cloud exposes /embeddings" parenthetical annotated as superseded by D-051. |
|
||||
| Adoption/UX | AT-RISK → fixed | Happy-path steps 2-4 contradicted until ingest status defined (G-102); verify-CTA/404/409 states well-specified; sitemap URL-resolvability pinned (G-109). |
|
||||
|
||||
## Binding Decisions (applied to PLAN.md before execute)
|
||||
|
||||
- **G-101** — Do NOT remove `generateStaticParams` (E87 build break under `output:'export'`). Keep it returning one placeholder param per route; export one client shell; add FastAPI catch-all `GET /marketplace/jobs/{id}` + `/employers/{id}` (trailing-slash variants) serving the shell — same routes-beat-the-mount mechanism as sitemap. [Vertical-slice integrity]
|
||||
- **G-102** — Ingest-status contract: seed-port postings enter `published` with original `postedAt`; admin-authored start `draft`. [Adoption/UX]
|
||||
- **G-103** — Board fetch = paged fetch-all (≤10⁴ documented); API returns `total_published`; "Showing X of Y" over the fetched set with honest truncation notice. [Performance]
|
||||
- **G-104** — Confirmed learner profiles persist in the marketplace store as a 6th table `learner_profile` (learner_id PK, profile JSON, confirmed_at, model) — NOT a filesystem sidecar. D-049 amended five→six tables. [Scope discipline]
|
||||
- **G-105** — Unset `AI_PUBLIC_BASE_URL`: loud G-11 boot warning; sitemap serves empty static set + warning (never malformed/relative URLs); robots omits the Sitemap directive; boot never crashes. [Security]
|
||||
- **G-106** — Match scores appear only in search context; cards without an active query show no score; per-learner profile-driven card scores post-v0.6; REQ-6-003 "scores on job cards" satisfied conditionally — recorded. [Adoption/UX]
|
||||
- **G-107** — "Mock matchScore absent" scoped to marketplace-path API payloads + marketplace surfaces; grep excludes packages/mock-data and employer/admin route groups. [Testability]
|
||||
- **G-108** — `admin_token` as `SecretStr`; compare via `secrets.compare_digest`. [Security]
|
||||
- **G-109** — Every sitemap job/employer URL must serve 200 on the single-port app (G-101 shells); board ItemList JSON-LD enumerates static structure only — never runtime postings. [Operability]
|
||||
- **G-110** — P1 web tests pin the E87 regression class itself (shells emitted; catch-all serves arbitrary ids; `notFound()` at non-placeholder id via full page load); the honest-501 stub pin at tests/identity/test_identity.py:444 rewritten onto the marketplace router in the same phase. [Testability]
|
||||
|
||||
## Plan Edits Applied
|
||||
|
||||
1. Task 1-3-04 + MH-1e rewritten per G-101 (placeholder shells + catch-all routes; E87 test class; stub-pin rewrite)
|
||||
2. Task 1-2-01 + MH-1f ingest-status contract (G-102)
|
||||
3. Task 1-3-02 paged fetch-all + total_published (G-103)
|
||||
4. Task 4-B-03 learner_profile table replaces the JSON sidecar (G-104; D-049 → six tables)
|
||||
5. Task 5-2-01 + MH-5a/5b unset-origin behavior + URL-resolvability (G-105/G-109)
|
||||
6. Task 3-3-01 + MH-3d matchScore scope + no-query-no-score (G-106/G-107)
|
||||
7. Task 1-2-03 SecretStr + compare_digest (G-108)
|
||||
8. PROJECT.md A-404 stale parenthetical annotated as superseded by D-051 probe
|
||||
|
||||
---
|
||||
|
||||
# Nextcraft v0.3 — GRILL.md (Adversarial Review Verdict)
|
||||
|
||||
**Stage:** GRILL, Phase 0 pre-execution · **Verdict:** GO-WITH-CHANGES · **Confidence:** 0.72
|
||||
|
||||
+115
-95
@@ -2,13 +2,15 @@
|
||||
|
||||
## Persona Roster
|
||||
|
||||
> **v0.6 update (RESEARCH, lead-developer assessment):** milestone = Marketplace Engine (D-047). **Activated:** data-engineer (6th D-027 store family — five tables, seed port, TS wire types dual-schema sync, its core constraint territory), ai-engineer full activation (tagging/enrichment/resume D-020 prompts + EmbeddingProvider protocol — LLM-facing work returns), frontend-engineer (client shells, engine-client marketplace extension, generateStaticParams removal ×2, SEO metadata + client JSON-LD), backend-engineer (module wiring, config surface: AI_EMBED_*/AI_ADMIN_TOKEN/AI_PUBLIC_BASE_URL, sitemap/robots routes). **security-auditor re-activated (phase-specific)** for admin-token fail-closed (P1), resume upload bounds + PII-logs (P4), sitemap origin (P5), final review (P6). **Deactivated:** identity-engineer (v0.5 custom — module shipped; only the `require_verified_adult` gate import is reused), voice-engineer, sandbox-engineer (no voice/sandbox/telemetry work), cli-engineer, design-system-engineer (existing primitives cover the marketplace shells).
|
||||
|
||||
> **v0.5 update (RESEARCH, lead-developer assessment):** milestone = Real Server Voice, KYC/Identity, Sandbox Environments, Seq-Lease. **Reactivated:** voice-engineer (real server STT/TTS, its v0.3 territory), sandbox-engineer (design/sim environment kinds + the seq-ack protocol on the fabric/agent it owns), frontend-engineer (defense audio upload, identity enrollment flow, kind-aware build surface). **New custom persona: identity-engineer** (KYC/identity domain — 5th store, provider protocol, age-gate dependencies, PII hygiene). **security-auditor re-activated (phase-specific)** for identity PII + age-gate bypass + audio upload attack surface (phases 1-4 review, final phase). ai-engineer light-touch (no LLM-facing work this milestone). backend-engineer retains settings/factory wiring + turbo/root scripts. cli-engineer inactive (v0.3.6 hotfix shipped; no CLI work planned). design-system-engineer/data-engineer inactive (one TS types extension only — data-engineer light-touch for variants/identity types).
|
||||
|
||||
### lead-developer
|
||||
```yaml
|
||||
active: true
|
||||
phase_specific: false
|
||||
reason: Coordinates the four v0.5 seams across voice/identity/sandbox/telemetry territories; resolves wave-order (D-046: seq-lease first) and identity-gate composition (D-043) boundaries
|
||||
reason: Coordinates the v0.6 marketplace territory split (store/ingest/embedding/tagging/search/enrichment/resume across data/ai/backend/frontend personas); owns wave-order coupling decisions (D-059: P1 ships backend WITH client shells)
|
||||
domain: coordination
|
||||
frameworks:
|
||||
- next.js
|
||||
@@ -27,77 +29,59 @@ territory:
|
||||
- "apps/ai-service/pyproject.toml"
|
||||
```
|
||||
|
||||
### voice-engineer
|
||||
### data-engineer
|
||||
```yaml
|
||||
active: true
|
||||
phase_specific: false
|
||||
reason: v0.3 territory reactivated — owns REQ-5-001/002: OpenAIAudioProvider (D-040) on the shared httpx pool, factory signature change, defense route fixes (fmt strip, size guard, media_type), descriptor mode=server, web audio POST
|
||||
domain: ai-media
|
||||
reason: v0.6 core activation — owns the 6th D-027 store family (D-049: posting/employer/embedding/application/employer_enrichment tables, WAL/pragmas/portable columns/validate hooks), the seed port from packages/mock-data jobs.ts+employers.ts (D-050, one-time, matchScore dropped), and the TS marketplace wire types (dual-schema sync with pydantic contracts)
|
||||
domain: data
|
||||
frameworks:
|
||||
- httpx
|
||||
- fastapi
|
||||
- pytest
|
||||
- react
|
||||
constraints:
|
||||
- provider-agnostic-protocol (D-030 drop-in; descriptor wins selection)
|
||||
- never-call-cloud-in-tests (MockTransport byte-contract pins)
|
||||
- key-redaction (mirror openai_compat _sanitize)
|
||||
- bounded-audio-in-memory (10MB guard before provider call)
|
||||
territory:
|
||||
- "apps/ai-service/ai_service/voice/**"
|
||||
- "apps/ai-service/tests/voice/**"
|
||||
- "apps/web/components/learner/defense-session.tsx"
|
||||
```
|
||||
|
||||
### identity-engineer
|
||||
```yaml
|
||||
active: true
|
||||
phase_specific: false
|
||||
reason: v0.5 custom persona (RESEARCH, D-042/43) — owns the new identity module: IdentityProvider protocol + mock, 5th SQLite store (DefenseStore pattern), /v1/identity router, age-gate dependencies (allowlist → identity → rate caps), derived age_band + document refs (PII minimal), caplog sentinel scrub test
|
||||
domain: identity
|
||||
frameworks:
|
||||
- fastapi
|
||||
- sqlmodel
|
||||
- pytest
|
||||
- pydantic
|
||||
- typescript
|
||||
constraints:
|
||||
- pii-never-stored-raw (refs + derived bands only, A-305)
|
||||
- pii-never-logged (caplog sentinel pin)
|
||||
- mock-verdict-honesty (mock marker rides every response, A-304)
|
||||
- gate-composition-order (G-5 allowlist first, identity second, 429 caps last)
|
||||
- schema-first
|
||||
- type-safe
|
||||
- dual-schema-sync (TS/Python changes made in both places)
|
||||
- idempotent-upserts ((source, external_id) + (posting_id, learner_id) unique constraints)
|
||||
territory:
|
||||
- "apps/ai-service/ai_service/identity/**"
|
||||
- "apps/ai-service/tests/identity/**"
|
||||
- "apps/ai-service/ai_service/marketplace/base.py"
|
||||
- "apps/ai-service/ai_service/marketplace/store.py"
|
||||
- "apps/ai-service/ai_service/marketplace/ingest.py"
|
||||
- "packages/types/**"
|
||||
- "packages/mock-data/**"
|
||||
```
|
||||
|
||||
### sandbox-engineer
|
||||
### ai-engineer
|
||||
```yaml
|
||||
active: true
|
||||
phase_specific: false
|
||||
reason: v0.3 territory reactivated — owns REQ-5-005/006 (template-layer env registry, starter contents, exec command policy) AND REQ-5-007 (seq-ack frame in ingest + agent spool trim, mid-burst regression test) — both live on the fabric/agent surfaces it built
|
||||
domain: infra
|
||||
reason: v0.6 full activation — LLM-facing work returns: tagging prompt+schema (D-052), enrichment extraction (D-053), resume parse extraction (D-054) — all through the existing D-020 structured_completion layer; plus the EmbeddingProvider protocol + deterministic feature-hash mock (D-051). Guards the agent/engine boundaries the marketplace surfaces touch
|
||||
domain: ai
|
||||
frameworks:
|
||||
- python
|
||||
- linux-namespaces
|
||||
- pydantic
|
||||
- httpx
|
||||
- pytest
|
||||
constraints:
|
||||
- stdlib-only-agent (AST-pinned sandbox-agent.py imports)
|
||||
- acks-are-advisory (gap detection stays authoritative; G-3/G-4 unchanged)
|
||||
- thread-safe-spool-trim (under _emit_lock, atomic Spool.rewrite)
|
||||
- digest-kind-agnostic (features derive from event kinds — pinned)
|
||||
- provider-agnostic-protocol
|
||||
- never-call-cloud-in-tests
|
||||
- mock-first (structured_completion runs against MockLLMProvider)
|
||||
territory:
|
||||
- "apps/ai-service/ai_service/sandbox/**"
|
||||
- "apps/ai-service/ai_service/telemetry/**"
|
||||
- "apps/ai-service/scripts/sandbox-agent.py"
|
||||
- "apps/ai-service/ai_service/variants/templates.py"
|
||||
- "apps/ai-service/tests/sandbox/**"
|
||||
- "apps/ai-service/tests/telemetry/**"
|
||||
- "apps/ai-service/ai_service/marketplace/tagging.py"
|
||||
- "apps/ai-service/ai_service/marketplace/enrichment.py"
|
||||
- "apps/ai-service/ai_service/marketplace/resume.py"
|
||||
- "apps/ai-service/ai_service/marketplace/embedding/**"
|
||||
- "apps/ai-service/ai_service/marketplace/search.py"
|
||||
- "apps/ai-service/ai_service/llm/**"
|
||||
- "apps/ai-service/ai_service/agents/structured.py"
|
||||
- "apps/ai-service/ai_service/prompts/**"
|
||||
```
|
||||
|
||||
### backend-engineer
|
||||
```yaml
|
||||
active: true
|
||||
phase_specific: false
|
||||
reason: Owns the settings surface both new providers hang off (voice_base_url/key/models, identity provider selection), factory + main.py lifespan wiring (voice factory gains http_client), .env.example documentation
|
||||
reason: Owns the v0.6 config surface (AI_EMBED_PROVIDER/BASE_URL/MODEL/API_KEY, AI_ADMIN_TOKEN, AI_PUBLIC_BASE_URL — all env-only), main.py lifespan wiring (marketplace store + embedding provider factory on app.state), the /v1/marketplace/* + admin routers' DI, sitemap/robots routes (D-056), .env.example documentation
|
||||
domain: backend
|
||||
frameworks:
|
||||
- fastapi
|
||||
@@ -108,12 +92,16 @@ frameworks:
|
||||
constraints:
|
||||
- secrets-via-env-only (D-014; keys never in code or commits)
|
||||
- idempotent-scripts
|
||||
- fail-closed-admin (unset AI_ADMIN_TOKEN = admin routes 403 + loud boot warning)
|
||||
territory:
|
||||
- "apps/ai-service/ai_service/config.py"
|
||||
- "apps/ai-service/ai_service/main.py"
|
||||
- "apps/ai-service/ai_service/voice/factory.py"
|
||||
- "apps/ai-service/ai_service/api/marketplace.py"
|
||||
- "apps/ai-service/ai_service/api/marketplace_admin.py"
|
||||
- "apps/ai-service/ai_service/api/deps.py"
|
||||
- "apps/ai-service/scripts/**"
|
||||
- "apps/ai-service/.env.example"
|
||||
- "apps/ai-service/pyproject.toml"
|
||||
- "package.json"
|
||||
- "turbo.json"
|
||||
```
|
||||
@@ -122,7 +110,7 @@ territory:
|
||||
```yaml
|
||||
active: true
|
||||
phase_specific: false
|
||||
reason: Reactivated for v0.5 UX: defense-session audio POST (recorded blob upload), identity enrollment flow (submit → pending → verified states + verify-CTA surfaces), build-surface kind-awareness (variant environment + test_command), engine-client identity + audio functions
|
||||
reason: v0.6 marketplace surfaces go live: board/detail/employer-profile client shells (D-055, build/defend pattern), engine-client marketplace functions (listPostings/getPosting/getEmployer/searchPostings/apply), generateStaticParams removal on jobs/[jobId]+employers/[employerId] ONLY, apply-flow wiring on the real route, SEO metadata (export const metadata on static set) + client-side JobPosting/Organization JSON-LD on runtime pages
|
||||
domain: frontend
|
||||
frameworks:
|
||||
- react
|
||||
@@ -131,58 +119,106 @@ frameworks:
|
||||
constraints:
|
||||
- component-first
|
||||
- server-components-default
|
||||
- honest-state-surfaces (provider badge: mock vs browser vs server; unverified labels)
|
||||
- honest-state-surfaces (404 on missing posting, verify-CTA on 403, empty boards)
|
||||
- no-build-time-db-reads (D-039 export build stays repo-only)
|
||||
territory:
|
||||
- "apps/web/**"
|
||||
- "packages/ui/**"
|
||||
- "packages/types/**"
|
||||
```
|
||||
|
||||
### security-auditor
|
||||
```yaml
|
||||
active: true
|
||||
phase_specific: true
|
||||
reason: v0.5 re-activated — identity PII (storage + logs), age-gate bypass review, audio upload attack surface (10MB guard, format confusion), exec command policy (per-kind allowlist), TTS media_type. Phases 1-4 reviews + final phase
|
||||
reason: v0.6 re-activated — admin token fail-closed + route coverage (P1), apply gate composition preserved D-043 (P1), embedding provider key redaction (P3), resume upload bounds + raw-bytes-never-logged (P4), enrichment provenance honesty (P4), sitemap origin config (P5), final review (P6)
|
||||
domain: security
|
||||
frameworks:
|
||||
- pytest
|
||||
- httpx
|
||||
constraints:
|
||||
- STRIDE-classified
|
||||
- pii-never-stored-raw
|
||||
- pii-never-logged
|
||||
- bounded-uploads
|
||||
- fail-closed (unset admin token never opens routes)
|
||||
- bounded-uploads (resume 2MB/format guard)
|
||||
- secrets-never-logged (_sanitize key redaction)
|
||||
territory:
|
||||
- "apps/ai-service/ai_service/identity/**"
|
||||
- "apps/ai-service/ai_service/api/defense.py"
|
||||
- "apps/ai-service/ai_service/api/sandboxes.py"
|
||||
- "apps/ai-service/ai_service/voice/**"
|
||||
- "apps/ai-service/ai_service/api/marketplace.py"
|
||||
- "apps/ai-service/ai_service/api/marketplace_admin.py"
|
||||
- "apps/ai-service/ai_service/marketplace/resume.py"
|
||||
- "apps/ai-service/ai_service/marketplace/embedding/openai_compat.py"
|
||||
- "apps/ai-service/ai_service/api/identity.py"
|
||||
```
|
||||
|
||||
### ai-engineer
|
||||
### identity-engineer
|
||||
```yaml
|
||||
active: true
|
||||
active: false
|
||||
phase_specific: false
|
||||
reason: Light-touch v0.5 — no LLM-facing work (voice STT/TTS is media plumbing, not model work; examiner agent unchanged); guards the agent/engine boundaries the new surfaces touch
|
||||
domain: ai
|
||||
reason: v0.5 custom persona — module shipped complete (identity/, gates, /v1/identity/*); v0.6 only REUSES the require_verified_adult import on the marketplace apply route (D-058). No identity-domain work.
|
||||
domain: identity
|
||||
frameworks:
|
||||
- pydantic
|
||||
- httpx
|
||||
- fastapi
|
||||
- sqlmodel
|
||||
- pytest
|
||||
constraints:
|
||||
- pii-never-stored-raw
|
||||
- pii-never-logged
|
||||
- mock-verdict-honesty
|
||||
- gate-composition-order
|
||||
territory:
|
||||
- "apps/ai-service/ai_service/identity/**"
|
||||
- "apps/ai-service/tests/identity/**"
|
||||
```
|
||||
|
||||
### voice-engineer
|
||||
```yaml
|
||||
active: false
|
||||
phase_specific: false
|
||||
reason: v0.5 territory — voice shipped complete (openai-audio provider, defense routes, browser SR/TTS client); no v0.6 voice work. The EmbeddingProvider protocol MIRRORS the voice pattern (D-051) but the voice module itself is untouched.
|
||||
domain: ai-media
|
||||
frameworks:
|
||||
- httpx
|
||||
- fastapi
|
||||
- pytest
|
||||
- react
|
||||
constraints:
|
||||
- provider-agnostic-protocol
|
||||
- never-call-cloud-in-tests
|
||||
- key-redaction
|
||||
- bounded-audio-in-memory
|
||||
territory:
|
||||
- "apps/ai-service/ai_service/llm/**"
|
||||
- "apps/ai-service/ai_service/agents/**"
|
||||
- "apps/ai-service/ai_service/prompts/**"
|
||||
- "apps/ai-service/ai_service/voice/**"
|
||||
- "apps/ai-service/tests/voice/**"
|
||||
- "apps/web/components/learner/defense-session.tsx"
|
||||
```
|
||||
|
||||
### sandbox-engineer
|
||||
```yaml
|
||||
active: false
|
||||
phase_specific: false
|
||||
reason: v0.3/v0.5 territory — fabric + env kinds + seq-ack shipped complete; no v0.6 sandbox/telemetry work (marketplace data plane never enters the namespace fabric).
|
||||
domain: infra
|
||||
frameworks:
|
||||
- python
|
||||
- linux-namespaces
|
||||
- pytest
|
||||
constraints:
|
||||
- stdlib-only-agent
|
||||
- acks-are-advisory
|
||||
- thread-safe-spool-trim
|
||||
- digest-kind-agnostic
|
||||
territory:
|
||||
- "apps/ai-service/ai_service/sandbox/**"
|
||||
- "apps/ai-service/ai_service/telemetry/**"
|
||||
- "apps/ai-service/scripts/sandbox-agent.py"
|
||||
- "apps/ai-service/ai_service/variants/templates.py"
|
||||
- "apps/ai-service/tests/sandbox/**"
|
||||
- "apps/ai-service/tests/telemetry/**"
|
||||
```
|
||||
|
||||
### cli-engineer
|
||||
```yaml
|
||||
active: false
|
||||
phase_specific: false
|
||||
reason: v0.3 persona — CLI shipped complete (v0.3.6 daemon surface); no v0.5 CLI work planned. Reactivated if milestone work touches apps/cli.
|
||||
reason: No CLI work in v0.6 (the binary attaches to every release per the ongoing-binaries requirement; the ship workflow handles it — no apps/cli changes).
|
||||
domain: cli
|
||||
frameworks:
|
||||
- node
|
||||
@@ -206,7 +242,7 @@ territory:
|
||||
```yaml
|
||||
active: false
|
||||
phase_specific: false
|
||||
reason: No design-token or primitive work planned in v0.5 (existing primitives — MicControl, GradeBadge, TranscriptViewer — cover the voice surfaces); roster retained.
|
||||
reason: No design-token or primitive work in v0.6 (existing primitives + JobFilters cover the marketplace shells); roster retained.
|
||||
domain: frontend
|
||||
frameworks:
|
||||
- tailwindcss
|
||||
@@ -218,28 +254,11 @@ territory:
|
||||
- "packages/ui/**"
|
||||
```
|
||||
|
||||
### data-engineer
|
||||
```yaml
|
||||
active: false
|
||||
phase_specific: false
|
||||
reason: Light-touch via frontend-engineer territory (variants/identity TS type extensions); no schema/mock-data work beyond the two typed additions.
|
||||
domain: data
|
||||
frameworks:
|
||||
- typescript
|
||||
constraints:
|
||||
- schema-first
|
||||
- type-safe
|
||||
- dual-schema-sync (TS/Python changes made in both places)
|
||||
territory:
|
||||
- "packages/types/**"
|
||||
- "packages/mock-data/**"
|
||||
```
|
||||
|
||||
## Phase-Specific Personas
|
||||
|
||||
| Persona | Phases | Removed After |
|
||||
|---------|--------|---------------|
|
||||
| security-auditor | 1 (seq-ack/agent), 2 (audio upload), 3 (identity PII), 4 (exec policy), 5 (final review) | milestone complete |
|
||||
| security-auditor | 1 (admin token + apply gates), 3 (embedding key redaction), 4 (resume bounds + PII), 5 (sitemap origin), 6 (final review) | milestone complete |
|
||||
|
||||
All other personas span the milestone. Deactivated personas receive no tasks.
|
||||
|
||||
@@ -247,8 +266,9 @@ All other personas span the milestone. Deactivated personas receive no tasks.
|
||||
|
||||
| Conflict | Resolution |
|
||||
|----------|------------|
|
||||
| voice-engineer vs backend-engineer (factory.py) | backend-engineer owns factory.py settings wiring + signature; voice-engineer owns openai_audio.py + its factory branch (provider construction), consumes config via DI |
|
||||
| sandbox-engineer vs frontend-engineer (build-surface) | sandbox-engineer owns engine-side kinds/templates/policy; frontend-engineer owns the web surface + client session hook; wire contract = VariantResponse TS types |
|
||||
| identity-engineer vs sandbox-engineer (gates) | identity-engineer owns the IdentityGate dependencies; sandbox-engineer owns the sandbox route they mount on (gate order D-043 is a joint review) |
|
||||
| security-auditor vs identity-engineer | identity-engineer implements; security-auditor reviews + may patch security defects directly in identity/ + api/defense.py (its territory) |
|
||||
| data-engineer vs backend-engineer (store.py / api routes) | data-engineer owns base.py/store.py/ingest.py schema + persistence; backend-engineer owns config.py/main.py/api routers/DI wiring — the marketplace store is consumed via deps.py, never imported directly by routes |
|
||||
| data-engineer vs ai-engineer (embedding storage) | data-engineer owns the embedding TABLE + BLOB codec; ai-engineer owns the EmbeddingProvider protocol + mock hash + search cosine math; the store's put/get_vector is the seam |
|
||||
| ai-engineer vs backend-engineer (factory/config) | backend-engineer owns config surface + factory wiring in main.py; ai-engineer owns provider implementations (openai_compat.py + mock.py) |
|
||||
| frontend-engineer vs data-engineer (wire types) | data-engineer owns packages/types marketplace shapes (dual-schema sync); frontend-engineer consumes them in engine-client + shells; wire contract = pydantic contracts in marketplace/base.py |
|
||||
| security-auditor vs backend-engineer (admin routes) | backend-engineer implements require_admin; security-auditor reviews fail-closed behavior + may patch security defects directly in api/marketplace_admin.py + resume.py (its territory) |
|
||||
| lead-developer vs any | lead-developer coordinates only, does not directly modify code files |
|
||||
+225
-166
@@ -1,182 +1,241 @@
|
||||
# Nextcraft v0.5 — PLAN.md
|
||||
# Nextcraft — PLAN.md (v0.6 Marketplace Engine)
|
||||
|
||||
## Overview
|
||||
|
||||
**Milestone:** v0.5 — Real Server Voice, KYC/Identity, Sandbox Environments, Seq-Lease
|
||||
**Tag line:** v0.4.x patches (P0 → v0.4.0 … P5 → v0.4.5 = milestone release)
|
||||
**Branch:** milestone/v0.5-real-voice-identity-envs
|
||||
**Milestone:** v0.6 — Marketplace Engine (D-047): the real marketplace backend behind the existing surfaces.
|
||||
**Tag line:** v0.5.x patches (P0 → v0.5.0 … P6 → v0.5.6 = milestone release) on the v0.5.x line
|
||||
**Branch:** milestone/v0.6-marketplace-engine
|
||||
|
||||
**Environment facts (probe-verified, apply throughout):** python 3.11.2 (venv at apps/ai-service/.venv), node v24.15.0, pnpm 12.3.4, no docker/podman/sudo, `unshare` userns verified working, SQLite is the only persistence (D-027), tests NEVER call the cloud (mock providers + httpx MockTransport), secrets only in gitignored `.ciagent/.env.secrets` / `.env*` (D-006/D-014). Runtime state lives in `~/.nextcraft/` (D-039) — tests pin to tmp dirs; sandbox workdir fixtures use the bind-mount-safe `sandbox_dir` fixture (overlayfs /tmp breaks userns binds).
|
||||
**Phase list:** P1 Postings backend + aggregation adapter (REQ-6-001) → P2 AI tagging (REQ-6-002) → P3 Semantic vector search (REQ-6-003) → P4 Enrichment + resume parsing (REQ-6-004/005, two parallel workstreams) → P5 SEO programmatic pages (REQ-6-006) → P6 Final review + ship.
|
||||
|
||||
**Wave order (D-046, binding):** P1 seq-lease → P2 voice → P3 identity → P4 environments → P5 final. Seq-lease lands first: it fixes the transport before environment phases add reconnecting telemetry producers; it touches no stores, no web, no settings.
|
||||
**Wave order (D-059, binding):** P1 ships the backend **WITH the web client shells** in the same phase — a live store behind mock-reading pages proves nothing; REQ-6-001's success criterion is surfaces reading real data. P2 before P3 because tag facets drive the sidebar filters AND the P3 posting embedding text includes the normalized tags. P4's two workstreams (enrichment / resume) are independent — parallelizable within the phase. P5 SEO depends only on P1 (routes exist to enumerate) but stays last-before-final: the sitemap needs `published` postings and the board metadata should reflect final facets. P6 final review + ship.
|
||||
|
||||
**Research decisions D-040..D-046 are binding design contracts.** This plan operationalizes them; it does not re-litigate them.
|
||||
**Research decisions D-048..D-059 are binding design contracts.** This plan operationalizes them; it does not re-litigate them. Assumptions A-401..A-412 are resolved fact.
|
||||
|
||||
**Environment facts (unchanged):** python 3.11.2 (venv at apps/ai-service/.venv, pip-only — A-009), node v24.15.0, pnpm 12.3.4, SQLite shared `Settings().db_path` (`~/.nextcraft/data/nextcraft.db`), tests NEVER call the cloud (mock providers + httpx MockTransport), secrets env-only (D-014), runtime state under `~/.nextcraft/` (D-039).
|
||||
|
||||
**Verification hooks (every phase, release-blocking):** `pnpm ai:test` (488 backend tests), web `tsx --test` (22), CLI `node:test` (61), `pnpm typecheck` (8 projects), `pnpm ai:lint` (ruff check), `pnpm build` (static export emits). **Hermeticity rule (v0.5 P04 lesson): every new test pins `Settings(db_path=tmp_path/...)` — never `~/.nextcraft`, never the cloud.**
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Postings Backend + Aggregation Adapter (REQ-6-001)
|
||||
|
||||
**Goal:** The 6th D-027 store family, the PostingSource adapter contract with the seeded corpus, the `/v1/marketplace/*` + admin API, the real apply route — and the web surfaces reading all of it at runtime.
|
||||
|
||||
### Wave 1-1: backend core
|
||||
|
||||
- **Task 1-1-01** (data-engineer): `ai_service/marketplace/base.py` — pydantic contracts per D-048: `Employer` (id, source, external_id, name + raw ingested fields), `IngestPosting` (A-402 shape: source, external_id, employer + posting fields), `Posting` (full row shape: status `draft|published|closed`, tags JSON column, portable scalar columns, posted_at), `PostingTags` (skills ≤10, seniority, category). Module mirrors D-031 boundary: never imports `agents/` or `api/`.
|
||||
- **Task 1-1-02** (data-engineer): `ai_service/marketplace/store.py` — `MarketplaceStore` protocol + SQLite impl per D-049 **(amended by G-104: six tables)** (`posting`, `employer`, `embedding`, `application`, `employer_enrichment`, `learner_profile`) on the shared `Settings().db_path`; identity/store.py mechanics exactly: WAL/synchronous=NORMAL/busy_timeout/foreign_keys pragmas, `sa_type` portable columns, `_validate()` hooks, surfaced IntegrityError, tz normalization. Unique constraints: `(source, external_id)` on posting + `(posting_id, learner_id)` on application (idempotent upserts A-402/A-408); employer unique per `(source, external_id)`; `embedding` table created now (posting_id PK, `vector` BLOB, `model`, `dim`), its codec/methods land in P3 (the data↔ai seam, per territory table); `learner_profile` (learner_id PK, profile JSON, confirmed_at, model — G-104, consumed in P4).
|
||||
- **Task 1-1-03** (backend-engineer): `config.py` — `admin_token: str = ""` (`AI_ADMIN_TOKEN`), `public_base_url: str = ""` (`AI_PUBLIC_BASE_URL`, placeholder consumed by P5 sitemap); `.env.example` documents both (keys never in code/commits). `api/deps.py` — `get_marketplace_store` (app.state DI; routes NEVER import the store directly — territory rule). `main.py` lifespan — `app.state.marketplace_store`; **loud boot warning when `admin_token` is unset** (G-11; admin routes stay 403-fail-closed regardless).
|
||||
|
||||
### Wave 1-2: ingest + routes
|
||||
|
||||
- **Task 1-2-01** (data-engineer): `ai_service/marketplace/ingest.py` — `PostingSource` protocol `fetch() -> Iterable[IngestPosting]`; `seeded_corpus` adapter over `marketplace/seed_corpus.json` — **a Python fixture file ported ONCE from `packages/mock-data/jobs.ts` (20 jobs) + `employers.ts` (10 employers)**, shared IDs per D-021 convention, mock `matchScore` DROPPED (D-051 computes real ones); `run(source)` upserts idempotently on `(source, external_id)` (employer rows upserted the same way). **Ingest-status contract (G-102): seed-port postings enter as `published` with their original `postedAt`; admin-authored postings start `draft`** — otherwise the board/facets/P1's success criterion are empty after ingest. No scheduler/crawler — the 3M+ corpus lands later as a third adapter with zero store/route changes.
|
||||
- **Task 1-2-02** (backend-engineer): `ai_service/api/marketplace.py` — public router: `GET /v1/marketplace/postings` (list, limit/offset, published only), `GET /v1/marketplace/postings/{id}` (404 honest), `GET /v1/marketplace/employers/{id}` (404 honest; raw fields now, merged+provenance in P4). Mounted in main.py. **The G-18 stub route MOVES here from `api/identity.py` and is deleted there** (identity keeps only its gate exports, D-048).
|
||||
- **Task 1-2-03** (backend-engineer + security-auditor): `ai_service/api/marketplace_admin.py` — `require_admin` FastAPI dependency (X-Admin-Token or Bearer vs `Settings().admin_token` as **`SecretStr`, compared via `secrets.compare_digest` (G-108)**): **unset token → every admin route 403, never open (D-057 fail-closed)**; routes: `POST /admin/postings` (create draft), `POST /admin/postings/{id}/publish` + `/close` (lifecycle `draft→published→closed`, invalid transition → 409), `POST /admin/ingest` (runs the seeded source), `GET /admin/applications` (review list, filterable by posting). Admin routes do NOT pass learner gates (different principal class, D-057).
|
||||
- **Task 1-2-04** (backend-engineer): apply route per D-058 — `POST /v1/marketplace/apply`, body `{posting_id}`: **gate composition binding (D-043, unchanged order): G-5 allowlist → `require_verified_adult` (imported from identity) → rate caps**; then posting must exist and be `published` (404 / 409); insert `application` row (status `submitted`); duplicate `(posting_id, learner_id)` → idempotent 200-with-existing; honest confirmation `{application_id, posting_id, status: "submitted"}`. All `stub`/`mock` markers die.
|
||||
|
||||
### Wave 1-3: web client shells (D-055)
|
||||
|
||||
- **Task 1-3-01** (data-engineer): `packages/types/marketplace.ts` — marketplace wire shapes (`PostingSummary`, `PostingDetail`, `EmployerProfile` with `{value, source}` provenance fields ready for P4, `ApplicationConfirmation`, `PostingFacets`) — **dual-schema sync with `marketplace/base.py`**: field-for-field parity, same change in both places.
|
||||
- **Task 1-3-02** (frontend-engineer): `apps/web/lib/engine-client.ts` marketplace section — `listPostings(filters)` (**paged fetch-all: walks limit/offset pages until exhausted, documented ≤10⁴ bound; response carries `total_published`; "Showing X of Y" computed over the fetched set with an honest truncation notice — G-103**), `getPosting(id)`, `getEmployer(id)`, `applyPosting(posting_id)` typed against the new `packages/types` shapes (`searchPostings` lands P3); 403 discrimination (verify-CTA vs allowlist) already works and is reused.
|
||||
- **Task 1-3-03** (frontend-engineer): board — `(marketplace)/marketplace/page.tsx` keeps its server shell; the grid + `JobFilters` become fetch-backed (client shell over `listPostings`); honest empty state (zero postings → message, never fake cards); filter option lists stay UI constants this phase (P2 derives them from real tags).
|
||||
- **Task 1-3-04** (frontend-engineer + backend-engineer): detail + employer shells **(G-101 — do NOT remove `generateStaticParams`; Next 15 hard-throws E87 under `output:'export'` for any dynamic route without it, even returning `[]`)**: keep `generateStaticParams` on `jobs/[jobId]` + `employers/[employerId]` returning ONE placeholder param (`{jobId: '_'}`) so exactly one client-shell page exports per route; add FastAPI catch-all routes `GET /marketplace/jobs/{id}` + `GET /marketplace/employers/{id}` (trailing-slash variants included — `trailingSlash: true`) serving that exported shell file — the same routes-beat-the-mount mechanism P5 relies on for sitemap; runtime client fetch → render, `notFound()` on 404; apply button wired: 403 verify-CTA renders the enrollment prompt, 409 closed renders honestly, success shows the confirmation payload. `packages/mock-data` jobs.ts/employers.ts go dormant for the marketplace path (still imported by employer/admin mockups — NOT deleted). Rewrite `tests/identity/test_identity.py:444`'s honest-501 pin onto the marketplace router in this phase (G-110).
|
||||
|
||||
### Wave 1-4: tests
|
||||
|
||||
- **Task 1-4-01** (data-engineer): store contract tests (tmp_path db — hermeticity rule): five tables created on the shared file, unique constraints fire on duplicates, pragmas + `_validate` hooks pinned; ingest idempotency (double-run → identical rows, no dupes); **seed-port fidelity check** (20 postings + 10 employers, IDs match the TS fixtures, `matchScore` absent from every API payload).
|
||||
- **Task 1-4-02** (backend-engineer + security-auditor): route tests — admin token fail-closed matrix (unset → 403 + boot warning in caplog; wrong/absent header → 403; correct → 200); lifecycle 409 on invalid transitions; apply gate composition (allowlist 403 fires FIRST for non-pilot ids, verify-CTA 403 for unverified, 429 at cap, 404/409 states, duplicate → 200 idempotent); 404s on missing postings/employers.
|
||||
- **Task 1-4-03** (frontend-engineer): web tests (`tsx --test` pattern) — engine-client marketplace functions; shells render fetched data; `notFound()` on 404; empty-board state; apply-flow states.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: AI Tagging (REQ-6-002)
|
||||
|
||||
**Goal:** Posting text → skills/seniority/category via D-020 structured output (mock-first, cloud-free); tags persisted; facets drive the sidebar.
|
||||
|
||||
### Wave 2-1: tagging + facets
|
||||
|
||||
- **Task 2-1-01** (ai-engineer): `marketplace/tagging.py` — `PostingTags` schema (skills `list[str]` ≤10, seniority enum, category enum) + versioned prompt constant; `tag_posting(text)` composes the EXISTING `structured_completion(provider, messages, model, schema, schema_hint)` (D-052: no new agent — one-shot extraction like grading's rubric call). Mock-first for free: mock provider default keeps tests cloud-free.
|
||||
- **Task 2-1-02** (data-engineer): persistence hooks — ingest pipeline tags every posting after upsert; re-tag on posting update **overwrites** the tags column (derived data, single source of truth — D-052; unlike enrichment, no append-only).
|
||||
- **Task 2-1-03** (backend-engineer): `PUT /v1/marketplace/admin/postings/{id}` (edit title/description/skills → triggers re-tag) + facets in API responses: `GET /v1/marketplace/postings` carries a `facets` payload (skills/seniority/category value counts) computed from stored tags of published postings.
|
||||
- **Task 2-1-04** (frontend-engineer): `JobFilters` facet lists derive from the API facets payload (static option lists die); counts render honestly.
|
||||
|
||||
### Wave 2-2: tests
|
||||
|
||||
- **Task 2-2-01** (ai-engineer): tests — tagging via `MockLLMProvider` (cloud-free pin); D-020 schema-violation → single bounded retry pinned; skills ≤10 enforced on an 11-skill mock response; facet derivation test (API facets exactly match stored tags of published-only postings); re-tag overwrite pin (old tags gone after update).
|
||||
|
||||
**Verification hooks P2:** all suites green (488+ / web 22+ / CLI 61 / typecheck / ruff / export build). Tagging tests stay mock-provider-only.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Semantic Vector Search (REQ-6-003)
|
||||
|
||||
**Goal:** EmbeddingProvider protocol (mock-first, deterministic), SQLite-local vectors, hybrid facet+cosine ranking, computed match scores replacing the mock `matchScore`.
|
||||
|
||||
### Wave 3-1: provider + storage
|
||||
|
||||
- **Task 3-1-01** (ai-engineer): `marketplace/embedding/base.py` — `EmbeddingProvider` protocol `embed(texts: list[str]) -> list[EmbeddingVector]` (frozen pydantic: `values: list[float]`, `model`, `dim` — D-051, D-030 mirror); `embedding/mock.py` — deterministic 256-dim feature-hash of normalized tokens (pure stdlib; similarity deterministic).
|
||||
- **Task 3-1-02** (ai-engineer): `embedding/openai_compat.py` — `POST {AI_EMBED_BASE_URL}/embeddings` (`{"model", "input": [texts]}`) on the shared httpx pool (D-017); `_sanitize` key redaction mirroring `openai_audio.py`. **Probe-corrected (D-051): ollama-cloud does NOT expose /embeddings — `AI_EMBED_BASE_URL` must point at another OpenAI-compatible endpoint (e.g. local ollama `:11434/v1`); default stays mock.**
|
||||
- **Task 3-1-03** (backend-engineer): `config.py` `embed_provider`/`embed_base_url`/`embed_api_key` (SecretStr)/`embed_model` + `embedding/factory.py` wiring in main.py lifespan: `AI_EMBED_PROVIDER=mock|openai-embed`, **G-11 boot-safe — `openai-embed` selected but unconfigured → loud log naming the fix, fallback to mock, boot never crashes**; `app.state.embedding_provider` + `deps.get_embedding_provider`.
|
||||
- **Task 3-1-04** (data-engineer): store `put_vector`/`get_vector` — struct-packed little-endian float32 BLOB codec (`<{dim}f`), `model`+`dim` persisted per row (A-403/A-404: lazy re-embed on provider change works); roundtrip test (the data↔ai seam per territory table).
|
||||
|
||||
### Wave 3-2: pipeline + ranking
|
||||
|
||||
- **Task 3-2-01** (ai-engineer): embed-on-publish — the publish transition embeds posting text + tags and stores the vector; **lazy re-embed**: a row whose `model`/`dim` ≠ the provider's is re-embedded on next access; candidate-side competency-profile text is embedded **on demand, never stored**.
|
||||
- **Task 3-2-02** (ai-engineer): `marketplace/search.py` — pure-Python cosine (sum of products / norm product; ≤10⁴ postings, no numpy — A-403); **hybrid per A-405/D-059: facet hard-filters (SQL on portable columns) first, then cosine ranking over the filtered set; no facets → pure semantic ranking**; `match_score` = cosine(candidate, posting); skill-breakdown = posting-tag overlap with the query + per-skill vector contribution (deterministic feature-hash dot-product shares — testable).
|
||||
- **Task 3-2-03** (backend-engineer): `GET /v1/marketplace/search` (`q` + facet params → ranked results with `match_score` + `skill_breakdown`); DI-composed, no store import in routes.
|
||||
|
||||
### Wave 3-3: web + tests
|
||||
|
||||
- **Task 3-3-01** (frontend-engineer): `engine-client.searchPostings(q, filters)`; board search bar wired to the search API; **match scores appear only in search context (G-106): cards without an active query show no score (honest absence, never a fabricated number); per-learner profile-driven card scores are explicitly post-v0.6 — REQ-6-003's "scores on job cards" satisfied conditionally in search context, recorded**; skill-breakdown panel on detail consumes the computed payload.
|
||||
- **Task 3-3-02** (ai-engineer + security-auditor): tests — deterministic similarity pins (same text → identical vector; related texts > unrelated texts cosine); codec roundtrip; factory fallback + boot-safe; `_sanitize` key-redaction pin (no key in any error); cosine math corner cases (zero vector); hybrid ordering (facet filters before ranking; pure-rank with no facets); embed-on-publish + lazy re-embed on model change.
|
||||
|
||||
**Verification hooks P3:** all suites green; embedding tests never leave the process (mock provider + MockTransport for openai_compat byte-contract tests).
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Enrichment + Resume Parsing (REQ-6-004, REQ-6-005)
|
||||
|
||||
**Goal:** Two independent workstreams, parallelizable (D-059). (A) employer enrichment with append-only provenance; (B) bounded resume upload → draft extraction → learner confirm gate.
|
||||
|
||||
### Workstream A: enrichment (REQ-6-004)
|
||||
|
||||
- **Task 4-A-01** (ai-engineer): `marketplace/enrichment.py` — `EmployerEnrichment` schema (description, social_links, logo_ref) + prompt; one-shot `enrich_employer()` via structured_completion (D-053, same D-020 layering as tagging; mock-first).
|
||||
- **Task 4-A-02** (data-engineer): store — `employer_enrichment` rows are **append-only** `(employer_id, field, value, source: llm|manual, model, created_at)`; `merge_employer(employer_id)` read precedence **manual > llm > ingested-raw** (A-409); manual override = new `manual` row, NEVER a mutation of `llm` rows.
|
||||
- **Task 4-A-03** (backend-engineer): **enrich-on-first-publish** — a publish whose employer has no prior published posting fires the one-shot extraction (re-publishing the same employer does not re-run); employer GET returns merged fields with per-field `{value, source}` provenance markers (A-304 honesty — LLM copy visibly marked); `POST /v1/marketplace/admin/enrichment` (manual-override upsert, D-057-guarded).
|
||||
- **Task 4-A-04** (frontend-engineer): employer profile shell consumes merged+marked fields; provenance badges visible per field.
|
||||
- **Task 4-A-05** (security-auditor + ai-engineer): tests — precedence pin (manual beats llm beats raw, each combination); append-only pin (no UPDATE/DELETE path exists on enrichment rows); marker honesty (mock-llm copy carries the llm marker end-to-end); trigger-once pin.
|
||||
|
||||
### Workstream B: resume parsing (REQ-6-005)
|
||||
|
||||
- **Task 4-B-01** (backend-engineer): pypdf pin — `pypdf>=6.19,<7` in `apps/ai-service/pyproject.toml` + idempotent install via the existing bootstrap venv channel (probe-verified 6.19.0, A-009; the ONLY new dep).
|
||||
- **Task 4-B-02** (ai-engineer): `marketplace/resume.py` — `ParsedProfile` schema mapped to the competency-profile shape (competencies, skills, experience bands) + prompt; parse via D-020 structured extraction; result carries `status: "draft"`.
|
||||
- **Task 4-B-03** (backend-engineer): `POST /v1/marketplace/resume` (multipart; **PDF/TXT/MD only, ≤2 MB, text-layer required** — empty pypdf extract → honest 422 "text layer required", NO OCR in v0.6; extracted text capped ~100K chars before the LLM call; raw bytes never stored, never logged — A-305, `_scrub_pii_validation` precedent) and `POST /v1/marketplace/resume/confirm` (learner-gated: G-5 → verified → caps; **persists the confirmed profile JSON to a 6th store table `learner_profile` (learner_id PK, profile JSON, confirmed_at, model — G-104; NOT a filesystem sidecar — the D-027 store family is the only persistence mechanism)**; the draft NEVER persists without confirm).
|
||||
- **Task 4-B-04** (frontend-engineer): learner dashboard profile section — upload → draft fields review → confirm; bounds errors render honestly (413 oversize / 422 format+text-layer); nothing saves before the confirm click.
|
||||
- **Task 4-B-05** (security-auditor + ai-engineer): tests — small **generated** PDF + TXT fixtures parse (cloud-free); bounds matrix (format rejection, >2 MB, empty-extract 422, char cap); draft-then-confirm flow; PII sentinel caplog test (resume bytes/sentinel strings in NO log record, NO stored raw form).
|
||||
|
||||
**Verification hooks P4:** all suites green; resume + enrichment tests stay mock-provider; resume fixtures generated in tmp (no binaries committed beyond minimal fixtures).
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: SEO Programmatic Pages (REQ-6-006)
|
||||
|
||||
**Goal:** Metadata + JSON-LD + sitemap/robots on the static-export fabric, D-039 unchanged.
|
||||
|
||||
### Wave 5-1: web metadata
|
||||
|
||||
- **Task 5-1-01** (frontend-engineer): build-time metadata — `export const metadata` (title/description/OG) on the **static set: landing, catalog/stack detail, marketplace board, pricing** (the existing layout.tsx/enroll pattern; static-export-compatible, probe 7).
|
||||
- **Task 5-1-02** (frontend-engineer): server-rendered JSON-LD on the board — `ItemList` + `Organization` — rendered into the export.
|
||||
- **Task 5-1-03** (frontend-engineer): client-side JSON-LD on the **runtime set** — `JobPosting` on job detail, `Organization` on employer profile, injected after fetch (A-411 accepted limitation: JS-less crawlers see the shell — recorded).
|
||||
|
||||
### Wave 5-2: sitemap/robots + tests
|
||||
|
||||
- **Task 5-2-01** (backend-engineer + security-auditor): `GET /sitemap.xml` + `GET /robots.txt` as plain FastAPI routes on the api app — **probe 6 verified: API routes beat the `StaticFiles("/")` mount**, so they win over the export in single-port production; enumerate the static page list (code constant) + all `published` postings; URLs from the `AI_PUBLIC_BASE_URL` origin; robots points at `/sitemap.xml`. **Unset `AI_PUBLIC_BASE_URL` (G-105): loud G-11 boot warning; sitemap serves the empty static-page set (never malformed/relative URLs) and robots omits the Sitemap directive (absolute-URL spec requirement); fail honest at request time; boot never crashes.** **Do NOT use Next `app/sitemap.ts`** (build-time enumeration cannot see runtime postings under `output: 'export'`).
|
||||
- **Task 5-2-02** (frontend-engineer + backend-engineer): tests — sitemap contents reflect published postings (draft/closed excluded); robots points at the sitemap with the configured origin; metadata/OG present in the exported HTML (`out/` probe); mount-order test (routes beat the static mount); client JSON-LD injection web test.
|
||||
|
||||
**Verification hooks P5:** all suites green + export build emits with metadata; sitemap tests pin `db_path=tmp_path` + `public_base_url` explicitly (hermetic, no `~/.nextcraft`).
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: Final Review + Ship (milestone release v0.5.6)
|
||||
|
||||
- **Task 6-1-01** (lead-developer → ci-review personas): multi-persona review of the full milestone branch diff (all phases — correctness, testing, security, performance, maintainability); **known follow-ups checked for scope creep: seed dormancy verified (mock-data untouched except marketplace path), employer dashboard stays mock (A-407), pricing stays mock (A-412) — zero payments/employer-auth/scheduler code**. P0s fixed in-phase.
|
||||
- **Task 6-1-02** (ci-audit): reconstruction test (git log ↔ .ciagent/ files), file/branch/commit discipline, tag hygiene; critical fixes in-phase.
|
||||
- **Task 6-1-03** (lead-developer → ci-ship): merge milestone → main; tag **v0.5.6** (milestone release); Gitea release with full summary + `nextcraft-linux-x64` + `.sha256` assets (D-036); delete milestone branches; mark REQ-6-001..006 complete; clear checkpoint.
|
||||
|
||||
---
|
||||
|
||||
## Must-Haves (per phase — concrete, falsifiable, test-linked)
|
||||
|
||||
### Phase 1
|
||||
|
||||
- **MH-1a**: Store contract tests (tmp_path db): five tables created on the shared `db_path`; `(source, external_id)` and `(posting_id, learner_id)` unique constraints fire on duplicates; WAL/pragmas/`_validate` hooks pinned.
|
||||
- **MH-1b**: Ingest idempotency — running the seeded source twice yields identical row counts (no dupes); upsert overwrites in place.
|
||||
- **MH-1c**: Admin fail-closed matrix — unset `AI_ADMIN_TOKEN` → every admin route 403 + boot warning in caplog; wrong/absent token → 403; correct token → 200. No admin route passes unguarded.
|
||||
- **MH-1d**: Apply gate composition **provable by test** — G-5 403 fires first, verify-CTA 403 second, 429 at cap; published-only (404 missing / 409 draft+closed); duplicate → 200-with-existing; confirmation payload exact; **G-18 stub markers gone** (grep + response-schema pin).
|
||||
- **MH-1e**: Web shells read real data — board/detail/employer consume `/v1/marketplace/*` (web tests pin the engine-client functions); `notFound()` on 404; honest empty board; **E87 regression class pinned (G-101/G-110): placeholder shells emitted for both routes (build does not throw), catch-all serves arbitrary ids, hydration + `notFound()` works at a non-placeholder id via full page load**; the honest-501 stub pin in tests/identity/test_identity.py:444 rewritten onto the marketplace router.
|
||||
- **MH-1f**: Seed fidelity — 20 postings + 10 employers, IDs match the TS fixtures, `matchScore` absent from all marketplace-path payloads; **seed postings enter `published` with original `postedAt` (G-102)**.
|
||||
|
||||
### Phase 2
|
||||
|
||||
- **MH-2a**: Tagging cloud-free via `MockLLMProvider`; `PostingTags` schema enforced (skills ≤10 pin on an 11-skill response); D-020 single bounded retry pinned on schema violation.
|
||||
- **MH-2b**: Tag-on-ingest + re-tag-on-update **overwrites** (old tags absent after update — single source of truth).
|
||||
- **MH-2c**: Facets derive from real stored tags — API facets payload matches published-only tags exactly; `JobFilters` option lists come from the payload (web test).
|
||||
|
||||
### Phase 3
|
||||
|
||||
- **MH-3a**: Codec roundtrip — struct-packed float32 BLOB via `put_vector`/`get_vector` with `model`+`dim` per row.
|
||||
- **MH-3b**: Deterministic similarity — same text → identical vector; related > unrelated cosine (pinned numbers).
|
||||
- **MH-3c**: Factory G-11 boot-safe — `openai-embed` unconfigured → boot survives with loud log + mock fallback; `_sanitize` redaction: no key string in any error (pinned).
|
||||
- **MH-3d**: Hybrid ordering — facets hard-filter before cosine ranking; no facets → pure semantic rank; `match_score` = computed cosine, **mock `matchScore` absent from marketplace-path API payloads and marketplace surfaces (G-107: the pin's grep excludes `packages/mock-data` and the employer/admin route groups, which stay mock)**; absent score = honest absence (no active query → no score, G-106).
|
||||
- **MH-3e**: Embed-on-publish + lazy re-embed on model change (pinned); candidate-side embeddings never stored.
|
||||
- **MH-3f**: Skill-breakdown = tag overlap + vector contribution, deterministic pin.
|
||||
|
||||
### Phase 4
|
||||
|
||||
- **MH-4a**: Enrichment append-only (no update/delete path) + precedence manual > llm > raw, every pairing pinned.
|
||||
- **MH-4b**: Provenance markers — every enriched field carries `{value, source}` in the API; mock-llm copy visibly marked end-to-end.
|
||||
- **MH-4c**: Enrich-on-first-publish fires exactly once per employer (re-publish no-op pin).
|
||||
- **MH-4d**: Resume bounds matrix — PDF/TXT/MD only (other → 422), >2 MB → 413, empty PDF extract → 422 "text layer required", ~100K char cap.
|
||||
- **MH-4e**: Draft-then-confirm — parse returns `status: "draft"`, nothing persisted; confirm persists; **PII sentinel caplog test green** (raw bytes never logged/stored).
|
||||
- **MH-4f**: pypdf pinned `>=6.19,<7` in pyproject + bootstrap installs it; generated PDF fixture parses.
|
||||
|
||||
### Phase 5
|
||||
|
||||
- **MH-5a**: Sitemap reflects published postings (draft/closed excluded) + static page list; origin from `AI_PUBLIC_BASE_URL`; **every sitemap job/employer URL must serve 200 on the single-port app via the G-101 catch-all shells (URL-resolvability pinned — G-109, never advertise URLs the deploy 404s)**; unset origin → empty set + warning, robots omits the Sitemap directive (G-105).
|
||||
- **MH-5b**: robots.txt points at `/sitemap.xml`.
|
||||
- **MH-5c**: Metadata + OG present in the exported HTML (`out/` probe); board ItemList+Organization JSON-LD server-rendered.
|
||||
- **MH-5d**: Runtime JSON-LD injected client-side after fetch (web test); JS-crawler limitation recorded.
|
||||
- **MH-5e**: Mount-order — sitemap/robots routes beat the static export in single-port mode (route test).
|
||||
|
||||
## Milestone Must-Haves
|
||||
|
||||
- **MH-M1**: All six requirements complete with their REQUIREMENTS.md success criteria: REQ-6-001 (store+lifecycle+routes+adapter, surfaces read real data, D-043 gates composed), REQ-6-002 (tags via D-020, facets drive filters), REQ-6-003 (protocol+SQLite vectors, similarity ranking, computed match scores), REQ-6-004 (enrichment with provenance), REQ-6-005 (resume auto-fill with review gate, bounded), REQ-6-006 (metadata+JSON-LD+sitemap/robots, D-039 unchanged).
|
||||
- **MH-M2**: Zero regressions on existing suites at every phase ship: 488 backend pytest, web tsx --test 22, CLI 61, typecheck 8, ruff, export build.
|
||||
- **MH-M3**: G-18 stub markers gone (MH-1d) — the one honest-501 marketplace route is now real.
|
||||
- **MH-M4**: The D-043 gate order provable by test (MH-1d) — allowlist → verified-18+ → caps, unchanged.
|
||||
- **MH-M5**: Hermeticity — every new test pins `db_path` (and any state dir) to `tmp_path`; no test touches `~/.nextcraft` or the cloud (v0.5 P04 lesson).
|
||||
- **MH-M6**: Scope discipline — pricing stays mock (A-412), employer dashboard stays mock (A-407), no payments/employer-auth/scheduler/crawler code; seed corpus is the only ingestion source.
|
||||
|
||||
## Risks
|
||||
|
||||
- **Dual-schema drift (TS wire types ↔ pydantic contracts)** — mitigated by the dual-schema-sync constraint (data-engineer owns both sides of every change) + parity checks in review.
|
||||
- **Match-score removal breaking non-marketplace mockups** — jobs.ts/employers.ts stay intact for employer/admin mockups; only the marketplace path switches (A-406); pinned by build + web tests.
|
||||
- **`AI_EMBED_BASE_URL` misconfiguration** (ollama-cloud has no /embeddings — D-051 probe) — default mock; G-11 boot-safe fallback with loud log; endpoint is config, never code.
|
||||
- **Admin token operational risk** — unset token locks the founder out of admin routes (fail-closed is the intent); loud boot warning names the fix; documented in README quickstart.
|
||||
- **Brute-force cosine at scale** — pilot-only (≤10⁴ postings, A-403); documented; `VectorStore` protocol keeps the pgvector swap API-compatible.
|
||||
- **pypdf text-layer variance across real resumes** — honest 422 on empty extract (no OCR); TXT/MD path always works; generated-fixture tests pin both.
|
||||
- **Client-shipped runtime fetch + JS-crawlers see shells on runtime pages** — accepted limitation (A-411), recorded; static set carries the SEO weight.
|
||||
- **E87 static-export build break (grill-verified, G-101)** — Next 15 `output:'export'` hard-throws for dynamic routes lacking `generateStaticParams` (even `[]`); mitigation: one placeholder param per route + FastAPI catch-all shell serving (routes beat the mount, probe 6); pinned by release-blocking tests (G-110).
|
||||
|
||||
## User-Facing Surface
|
||||
|
||||
1. **Voice defense with a provider badge** (`/defend/[competencyId]`): the learner's spoken answers upload as audio and are transcribed server-side when `AI_VOICE_PROVIDER=openai-audio` is configured; examiner questions play as server TTS audio; the mic control shows an honest badge — `server voice`, `browser voice`, or `mock` — derived from the provider descriptor (`VoiceDescriptor.mode`), and degrades visibly (browser/mock fallback) with keys absent.
|
||||
2. **Identity enrollment flow** (new `/enroll` learner route + marketplace surfaces): submit verification → pending state → verified/rejected state; verified learners proceed to variants/sandboxes/defense; unverified learners hitting gated routes see a structured verify-CTA (403 payload rendered as an actionable prompt, not a dead error). Mock verdicts are labeled `mock` everywhere they surface (A-304 honesty).
|
||||
3. **Environment-typed build flows** (`/build/[competencyId]`): design competencies open a design environment (SVG/HTML/schematic artifact starter files, Run = validator harness), simulation competencies open a simulation environment (benchmark script + dataset starter files, Run = bounded harness execution); the Run/Test buttons use the variant's real `test_command` instead of hardcoded pytest; the surface, file tree, editor, read-only output panel (CUT-2), and telemetry pulse are unchanged across kinds.
|
||||
4. **Invisible durability**: mid-connection kill of a build session loses nothing on reconnect (seq-ack protocol) — no visible UI, proven by tests.
|
||||
1. **Marketplace job board** (`/marketplace`): reads real published postings from `/v1/marketplace/postings`; facet sidebar derives from real AI tags; match scores are computed, not mocked.
|
||||
2. **Job detail** (`/marketplace/jobs/[jobId]`): real posting fetch; full description, tags, employer info, computed skill-breakdown; honest 404 on missing postings; apply flow live with verify-CTA on 403 and 409 on closed.
|
||||
3. **Employer profile** (`/marketplace/employers/[employerId]`): merged raw+enriched fields with visible per-field provenance markers.
|
||||
4. **Learner profile auto-fill**: resume upload → draft review → confirm (nothing saved silently).
|
||||
5. **Founder/admin surface via curl + engineering docs**: `AI_ADMIN_TOKEN`-guarded posting authoring, lifecycle, ingest trigger, enrichment override, applications review — documented in a README marketplace quickstart addition (curl examples included).
|
||||
6. **SEO**: sitemap/robots served by the API; metadata + JSON-LD on the page set.
|
||||
|
||||
## Happy Path
|
||||
## Happy Path (end-to-end, written BEFORE execute)
|
||||
|
||||
**Defense with real voice:** learner opens `/defend/cmp-*` → DefenseSession starts → examiner question streams (SSE) → learner speaks → MediaRecorder captures webm → POST `/v1/defense/{id}/answer` (multipart audio) → server strips codec param (`webm;codecs=opus` → `webm`), enforces ≤10MB, calls `OpenAIAudioProvider.transcribe` → transcript turn stored (STT latency recorded) → learner clicks the speaker icon on an examiner turn → GET `/v1/defense/{id}/audio/{turn_id}` → server TTS bytes stream back with correct media_type → verdict + integrity signals render unchanged.
|
||||
|
||||
**Identity-gated build:** learner completes `/enroll` (submit → mock provider verdict `verified`, age band 18+) → opens a design competency → POST `/v1/variants` passes the identity gate (verified ≥16) → variant carries `environment: "design"` + `test_command` → sandbox created (allowlist ✓ → identity ✓ → rate cap ✓) → starter files written → Run executes the validator harness in the namespace sandbox → telemetry streams with seq-acks → grade digest renders.
|
||||
1. Founder sets `AI_ADMIN_TOKEN=ops-secret` in `.env`; boots the stack; log shows the marketplace store on the shared DB (no unset-token warning).
|
||||
2. Founder runs `POST /v1/marketplace/admin/ingest` (X-Admin-Token header) → seeded corpus populates: 20 postings + 10 employers, upsert-idempotent.
|
||||
3. Founder authors a posting via `POST /v1/marketplace/admin/postings` (draft) → `POST .../publish` → posting is published (auto-tagged, embedded).
|
||||
4. Learner opens `/marketplace` → board client shell fetches real postings → filters by a facet derived from real tags → opens `/marketplace/jobs/{id}` detail.
|
||||
5. An 18+ verified, allowlisted learner clicks Apply → passes G-5 → verified-18+ → rate caps → posting published → `application` row (status `submitted`) → honest confirmation; a second click returns the same application (idempotent).
|
||||
6. Founder reviews via `GET /v1/marketplace/admin/applications` (token-guarded) and contacts the employer out-of-band (no notification infra, A-408).
|
||||
7. `GET /sitemap.xml` lists the static pages + every published posting.
|
||||
|
||||
## UX Acceptance Criteria
|
||||
|
||||
1. The mic control badge always tells the truth about which voice path is live (server/browser/mock) — never claims server when mock is wired.
|
||||
2. Unverified/under-age callers on gated routes get a 403 with an actionable verify-CTA payload (rendered as a prompt with a link to enrollment) — never a bare JSON error in the UI.
|
||||
3. Mock identity verdicts are visibly labeled `mock` in every surface that shows verification state.
|
||||
4. Design/sim environments are indistinguishable from build environments in surface mechanics (file tree, editor, Run/Test, output panel) — only starter contents and the Run command differ; no route changes, no new navigation.
|
||||
5. Run/Test buttons reflect the variant's `test_command` (no hardcoded pytest on a design competency).
|
||||
6. No durable state is written inside the repo (state in `~/.nextcraft/`; tests in tmp dirs).
|
||||
7. All existing accessibility baselines hold (WCAG AA contrast on new badge/CTA states).
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Seq-Lease + Replay Margin (REQ-5-007)
|
||||
|
||||
**Goal:** Close the one-line replay-margin/ACK gap (P07-documented): the capture agent requeues only `_last_sent` on detected disconnect while N frames may be in TCP flight — frames 1..N-1 are lost. Server seq-acks close it.
|
||||
|
||||
### Wave 1-1: ingest ack emission
|
||||
|
||||
- **Task 1-1-01** (sandbox-engineer): `telemetry/ingest.py` — emit `{"type":"seq_ack","seq":N}` after every successful `store.append` in `IngestSession._append` (N = durable `latest_seq` snapshot already computed; O(1), advisory — gap detection stays authoritative; G-3 flood semantics untouched). Unit test: appended seq N → ack frame N observed on the WS (TestClient portal pattern, `tests/api/test_telemetry_ingest.py`).
|
||||
- Files: `apps/ai-service/ai_service/telemetry/ingest.py`, `apps/ai-service/tests/telemetry/`
|
||||
- **MH-1a**: a test proves each successful append emits an ack carrying the post-append `latest_seq`.
|
||||
|
||||
### Wave 1-2: agent ack consumption + spool trim
|
||||
|
||||
- **Task 1-2-01** (sandbox-engineer): `scripts/sandbox-agent.py` — supervisor loop (currently discards all non-close frames) parses text frames; on `type == "seq_ack"` trims every spool/pending line with `seq <= ack` under `_emit_lock` via atomic `Spool.rewrite`; clears `_last_sent` if its seq ≤ ack; tolerates any frame interleaving (acks/gap_warning/rejected); keepalive pings (binary) unaffected. **Stdlib-only (AST-pinned).**
|
||||
- **Task 1-2-02** (sandbox-engineer): add an explicit spool bound (max lines, e.g. 4096 — documented; D-R07 correction: no cap existed) — oldest-beyond-bound dropped with a counter; **G-14: overflow is by-design gap creation — unit test proves dropped-counter > 0 → replayed trace exhibits gaps → grader/gap path marks it ungradable (never a silently-truncated-but-gradable trace); document the worst-case arithmetic (~64KB diff cap × 4096 lines ≈ 256MB, under but HALF the 512MB G-2 budget — the spool lives inside the swept workdir)**; document G-2+flood-cap as the outer bound.
|
||||
- Files: `apps/ai-service/scripts/sandbox-agent.py`, `apps/ai-service/tests/sandbox/test_sandbox_agent.py`
|
||||
- **MH-1b**: unit test — agent with a scripted WS that acks mid-drain trims its spool to `seq > ack` exactly (no over-trim, no under-trim), stays within the explicit bound, and overflow drops create honest gaps (ungradable, G-14).
|
||||
- **MH-1c**: `replay_margin()` behavior after acks: requeue window is bounded by unacked in-flight only (repeated reconnect/ack cycles never lose or duplicate a spooled line).
|
||||
|
||||
### Wave 1-3: mid-burst regression test (the real proof)
|
||||
|
||||
- **Task 1-3-01** (sandbox-engineer): extend `tests/telemetry/test_durability.py` with `test_midburst_disconnect_loses_nothing` — real uvicorn + KillableProxy; sever the connection **immediately after a rapid multi-frame send, WITHOUT waiting for server observation** (the exact scenario the P07 de-flake documented as uncovered); revive; assert every emitted seq stored exactly once, in order; assert spool trimmed to ≤ ack margin; keep `_await_events`/portal patterns (deterministic, no socket surgery).
|
||||
- Files: `apps/ai-service/tests/telemetry/test_durability.py`
|
||||
- **MH-1d**: the mid-burst test passes repeatedly (≥3 consecutive runs) with zero loss/dup outside the acked margin.
|
||||
|
||||
**Verification strategy P1:** `pnpm ai:test` (413+green), ruff, no web/TS changes, no settings changes. Existing durability + reconnect-flush suites stay green.
|
||||
|
||||
**Risks:** mid-burst determinism (mitigated: ack protocol is the fix; wait for server-side observation of the ack itself); `_emit_lock` reentrancy from supervisor thread (trim under the same lock as flush); frame-order tolerance.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Real Server Voice (REQ-5-001, REQ-5-002)
|
||||
|
||||
**Goal:** The `openai-audio` VoiceProvider — server STT/TTS for voice defense end-to-end when keys exist; mock/browser unchanged.
|
||||
|
||||
### Wave 2-1: settings + factory + provider
|
||||
|
||||
- **Task 2-1-01** (backend-engineer): `config.py` — add `voice_base_url: str = ""`, `voice_api_key: str = ""` (never logged), `voice_stt_model: str = "whisper-1"`, `voice_tts_model: str = "tts-1"`, `voice_tts_voice: str = "alloy"`, `voice_tts_format: Literal["mp3","wav","opus"] = "mp3"` (G-16: enum, not free string — it feeds a Content-Type), `voice_max_audio_mb: int = 10`; `.env.example` documents them (keys never in code/commits); unknown format value → fall back to default with a loud log (consistent with G-11).
|
||||
- **Task 2-1-02** (backend-engineer): `voice/factory.py` — `voice_provider_from_settings(settings, http_client)` (signature change); `openai-audio` branch constructs `OpenAIAudioProvider`; rejects selection when `voice_base_url`/`voice_api_key` empty with an actionable error for direct callers; **G-11: `main.py` lifespan catches the factory error, logs loudly (naming the missing vars), and falls back to the mock provider — a typo'd env in an unattended deploy must never crash the boot (D-039); descriptor then honestly reads `mock`**; lifespan passes `app.state.http_client` (state-injection preserved).
|
||||
- **Task 2-1-03** (voice-engineer): `voice/openai_audio.py` — `transcribe()`: multipart `POST {base}/audio/transcriptions` (`file` + `model`, response_format=json → `TranscriptSegment`); `synthesize()`: streaming `POST /audio/speech` (JSON body `model/input/voice/response_format`, raw byte chunks); `descriptor = VoiceDescriptor(mode="server", sr_available=True, tts_available=True, hint=...)`; errors sanitized with key redaction (mirror `llm/openai_compat.py:_sanitize`); reuses the shared httpx client (D-017; read=300s).
|
||||
- Files: `apps/ai-service/ai_service/config.py`, `voice/factory.py`, `voice/openai_audio.py`, `main.py`, `.env.example`
|
||||
- **MH-2a**: MockTransport byte-contract tests — STT: multipart fields + response parse → `TranscriptSegment`; TTS: JSON body + byte stream → concatenated chunks; failure pins 413/400/429/timeout → sanitized errors, NO key leak (pinned).
|
||||
- **MH-2b**: factory: `openai-audio` selected + configured → server-mode provider; selected + unconfigured → actionable rejection for direct callers AND app boot survives with mock fallback + loud log naming the fix (G-11); mock/browser unchanged; provider always carries a `descriptor` (a-15 — a missing descriptor would badge the server path as mock). Invert the v0.4 rejection test (`test_real_server_stt_tts_rejected_as_v04_seam`).
|
||||
|
||||
### Wave 2-2: defense route fixes + audio upload
|
||||
|
||||
- **Task 2-2-01** (voice-engineer): `api/defense.py` — fmt derivation strips codec params (`"webm;codecs=opus"` → `webm`; else real STT 400s); enforce `voice_max_audio_mb` BEFORE provider call (422 empty / 413 oversize; a-9: Content-Length fast path before buffering); TTS route media_type mapped from the `voice_tts_format` enum (G-16); descriptor now comes from the provider (mode=server flows to the client untouched).
|
||||
- **Task 2-2-02** (frontend-engineer): `engine-client.ts` — `answerDefense` audio variant (FormData: blob + filename + content-type); `defense-session.tsx` — POST the recorded blob instead of discarding it; **G-12: recording bound — auto-stop at a max duration (default 180s) with a visible timer, `recorder.start(timeslice)` for observable size (a-13); a 413 response renders as an honest "answer too long — re-record" prompt, never silent loss**; provider badge from descriptor (`server voice`/`browser voice`/`mock`) with visible degradation states.
|
||||
- Files: `apps/ai-service/ai_service/api/defense.py`, `apps/web/lib/engine-client.ts`, `apps/web/components/learner/defense-session.tsx`, `apps/web/tests/`
|
||||
- **MH-2c**: API test — webm;codecs=opus content-type reaches the provider as clean `webm`; oversize audio → 413 without provider call; empty → 422 (existing).
|
||||
- **MH-2d**: web tests — audio POST path builds correct FormData; badge reflects descriptor mode; auto-stop fires at the bound; 413 renders the re-record prompt (G-12).
|
||||
|
||||
**Verification strategy P2:** `pnpm ai:test`, ruff, `pnpm typecheck`, web tests, `pnpm build` (static export still emits). Manual cloud probe recipe documented in `.env.example` comments (never CI). Defense flow tests stay mock-only (cloud-free rule).
|
||||
|
||||
**Risks:** full audio bytes buffered in memory (bounded by the 10MB guard); TTS `input` ≤4096 chars (examiner questions are short — enforced with a guard + truncation error); factory signature change touches main.py lifespan (state-injection pattern preserved).
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Identity + Age-Gating (REQ-5-003, REQ-5-004)
|
||||
|
||||
**Goal:** Identity verification backend behind a provider protocol (mock-first); backend-enforced age gates composed with G-5.
|
||||
|
||||
### Wave 3-1: identity module core
|
||||
|
||||
- **Task 3-1-01** (identity-engineer): `ai_service/identity/base.py` — `IdentityProvider` protocol: `submit(learner_id, submission) -> submission_id`, `poll(submission_id) -> verdict {status, age_band, provider, mock, refs}`; `mock.py` — deterministic mock (approve-on-policy: age band from a scripted DOB field, reject scripted-bad); verdicts carry `mock: true` marker (A-304).
|
||||
- **Task 3-1-02** (identity-engineer): `ai_service/identity/store.py` — 5th D-027 store, DefenseStore pattern (WAL, `foreign_keys=ON`, portable columns, `@validates`): `identity_record` table — `id` (submission id, minted once), `learner_id` (indexed), `status: pending|verified|rejected`, `provider`, `provider_verdict` (JSON, mock-marked), `age_band` (derived `16-17`|`18+`, NEVER raw DOB), `document_refs` (JSON refs — raw documents NEVER stored), `submitted_at`, `verified_at`; insert-only + latest-per-learner lookup.
|
||||
- **Task 3-1-03** (identity-engineer): `main.py` lifespan — `app.state.identity_store` + `app.state.identity_provider` (state-injection overrides preserved); `config.py` — `identity_provider: str = "mock"`, identity store rides the same db_path.
|
||||
- **MH-3a**: store tests — insert/poll/latest/verdict provenance; constraints fire on invalid bands; same SQLite file (additive table, D-027 family).
|
||||
|
||||
### Wave 3-2: API surface + gates
|
||||
|
||||
- **Task 3-2-01** (identity-engineer): `api/identity.py` — `/v1/identity/submit` (submission → pending; G-13: one active pending per learner — resubmit while pending → 409 echoing the pending state; per-learner submit rate cap → 429), `/v1/identity/status/{learner_id}` (latest record + mock marker), `/v1/identity/verify/{submission_id}` (poll provider → verified/rejected transition); router mounted in main.py.
|
||||
- **Task 3-2-02** (identity-engineer): gate dependencies — `require_verified_age(min_age)` FastAPI dependencies; **composition order binding (D-043)**: G-5 allowlist (403 pilot guard) → identity verdict (403 + verify-CTA payload `{reason, min_age, current_status, verify_cta}`) → rate caps (429). Apply: school 16+ on variant generation (`api/variants.py`), sandbox create (`api/sandboxes.py`), defense start (`api/defense.py`); marketplace 18+ via `require_verified_adult` on ONE minimal gated route (`POST /v1/marketplace/apply` — G-18: honest stub; passes the gate composition then returns 501 with `stub: true` + mock-verdict markers, never a fabricated "applied" outcome).
|
||||
- **Task 3-2-03** (identity-engineer, G-9): conftest `verified_pilot` fixture — seeds an identity record (mock provider, band 18+ or 16+) for test learner ids + allowlist coverage in test settings; MUST land in the same wave as the gates or the existing variant/sandbox/defense suites 403 en masse (those routes are ungated today).
|
||||
- **Task 3-2-04** (security-auditor, phase-specific): PII review — sentinel scrub test: submit identity with sentinel PII strings → assert they appear in NO log record (caplog) and NO stored raw form (store inspection); API responses expose verdict + mock marker only.
|
||||
- Files: `apps/ai-service/ai_service/identity/**`, `api/identity.py`, `api/variants.py`, `api/sandboxes.py`, `api/defense.py`, `main.py`, `config.py`, `tests/identity/`, `tests/api/`
|
||||
- **MH-3b**: gate tests — verified 18+ passes all school gates; 16-17 passes school gates but 403s the marketplace route (honest stub response beyond the gate, G-18); unverified → 403 with verify-CTA payload; under-16 → 403 everywhere gated; allowlist rejection (403) still fires FIRST for non-pilot learner ids; identity submit: pending-resubmit → 409, rate cap → 429 (G-13); **all pre-existing variant/sandbox/defense API suites remain green under the `verified_pilot` fixture (G-9)**.
|
||||
- **MH-3c**: caplog sentinel test green (PII never logged/stored).
|
||||
|
||||
### Wave 3-3: web enrollment flow
|
||||
|
||||
- **Task 3-3-01** (frontend-engineer): `/enroll` route — submit → pending → verified/rejected states (honest, mock-labeled); engine-client identity functions; **G-10: client 403 discrimination — `verify_cta` present in the 403 payload → new `VerifyRequiredError` `{reason, min_age, current_status, verify_cta}`; allowlist detail → existing `NotAllowlistedError` (today engine-client.ts collapses every 403 into NotAllowlistedError — a verify-CTA would render as an allowlist lie)**; gated-route 403 CTA rendered as actionable prompt (link to `/enroll`); dashboard learner age badge reflects verified state.
|
||||
- Files: `apps/web/app/(learner)/enroll/`, `apps/web/lib/engine-client.ts`, `apps/web/components/`, `packages/types/`
|
||||
- **MH-3d**: web tests — identity client functions; CTA payload shape; enrollment states render; **403 discrimination: verify-CTA → VerifyRequiredError, allowlist detail → NotAllowlistedError (G-10)**. `pnpm build` emits the new route.
|
||||
- **MH-3e** (CUT-3): identity flow test via `TestClient` against real `create_app` (routers mounted, gates composed, real stores, mock providers) — unverified learner → variants POST → 403 verify-CTA → submit + verify (mock) → variants POST 200. No uvicorn harness (identity is plain JSON; the real-server harness stays where transport matters — P1/P4).
|
||||
|
||||
**Verification strategy P3:** `pnpm ai:test`, ruff, typecheck, web tests, `pnpm build`. PII caplog test is release-blocking (security-auditor sign-off).
|
||||
|
||||
**Risks:** self-asserted `learner_id` trust level (documented as pilot-scale — same as G-5 today; real auth is post-v0.5); shared-SQLite additive table (safe); mock-verdict honesty must ride every response (pinned).
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Design/Sim Environments (REQ-5-005, REQ-5-006)
|
||||
|
||||
**Goal:** Environment kinds at the template/variant layer; per-kind starter contents + exec policy; kind flows through telemetry; digest untouched.
|
||||
|
||||
### Wave 4-1: registry + wire types
|
||||
|
||||
- **Task 4-1-01** (sandbox-engineer): `variants/templates.py` — `TaskTemplate.environment: Literal["build","design","simulation"] = "build"` + per-kind `starter_files` + `harness_command`/`test_command` policy fields; **G-15: command fields validated at definition time (Python, where shlex exists) — must roundtrip `shlex.split` → whitespace-join → `shlex.split` identically (no quotes/globs/metachars; violation is a template-authoring bug caught in tests)**; add one design template (stack-designer c001/c002 — already sanctioned) + one simulation template (stack-science or stack-operator); `variants/generator.py` + `store.py` carry the field through `VariantRecord`.
|
||||
- **Task 4-1-02** (frontend-engineer): `api/variants.py` — `VariantResponse` gains `environment` + `test_command` (closes the dead-field gap); TS `packages/types/variants.ts` + engine-client types sync (dual-schema rule: both places, same change).
|
||||
- **MH-4a**: variant tests — design/sim templates generate kind-tagged variants with correct starter files + commands; command fields roundtrip the shlex validator (G-15); wire response carries both fields (a-11: required on the wire, TS required-field parity); TS types match Python field-for-field.
|
||||
|
||||
### Wave 4-2: exec command policy
|
||||
|
||||
- **Task 4-2-01** (sandbox-engineer + security-auditor): `api/sandboxes.py` exec route — per-kind command policy: **G-15: EXACT argv-token matching** against {template-declared harness/test argv[0]} ∪ a small generic file/nav set — never prefix/substring (trivially bypassed via flags/`-c` passthrough); `sh -c` passthrough DISALLOWED for design/sim kinds (the gaming vector: faking build-style test cycles into a kind-agnostic digest); violation → 422 naming the allowed set; policy table is code (reviewable, versioned); build-kind flows do not regress (existing tests green).
|
||||
- **MH-4b**: exec tests — design kind rejects pytest-style arbitrary commands not in policy (422); simulation kind accepts its declared harness; build kind flows unchanged.
|
||||
|
||||
### Wave 4-3: learner surface kind-awareness
|
||||
|
||||
- **Task 4-3-01** (frontend-engineer): `use-sandbox-session.ts` — `test()` uses `variant.test_command`; **G-15: TS splits on whitespace ONLY (no shlex in the browser — safe because templates validated quote-free at authoring, Task 4-1-01)**; `build-surface.tsx` — RunControls commands from the variant; starter-file materialization loop already kind-agnostic (verify against design/sim starter sets); honest busy/denied/error states carry over.
|
||||
- **MH-4c**: web tests — test command comes from the variant; Run button label/command per kind.
|
||||
|
||||
### Wave 4-4: telemetry + grading proof
|
||||
|
||||
- **Task 4-4-01** (sandbox-engineer): digest pin test — `compute_digest` over a synthetic design-kind trace (validator harness events) → same feature classes as build traces (kind-agnostic by construction — now pinned); telemetry capture agent unchanged (content-agnostic `_EVENT_KINDS` verified).
|
||||
- Files: `apps/ai-service/ai_service/variants/templates.py`, `generator.py`, `store.py`, `api/variants.py`, `api/sandboxes.py`, `grading/features.py` (tests only), `apps/web/hooks/use-sandbox-session.ts`, `apps/web/components/learner/build-surface.tsx`, `packages/types/variants.ts`
|
||||
- **MH-4d** (absorbs former MH-4e per G-17 — no hope-shaped must-haves): design-kind E2E in the real-server harness — design variant → sandbox → starter files → Run validator harness in-ns → telemetry flows → digest computes (mock LLM, real stores) with concrete assertions: stored seqs contiguous 0..N exactly once AND the agent's spool ends ≤ the ack margin (real agent; where a fake agent is used, cite the P1 suite as the ack/trim coverage instead of asserting).
|
||||
|
||||
**Verification strategy P4:** `pnpm ai:test`, ruff, typecheck, web tests, `pnpm build`. Grading digest diff vs build traces = zero behavioral drift (pinned).
|
||||
|
||||
**Risks:** starter-file materialization is client-driven (missing-file hazard — mitigate: starter sets are small + template-authored; document server-side materialization as a future seam); `test_command` is wire-visible (template-authored, not learner-authored — documented); dual TS/Python schema sync (rule enforced in review).
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Final Review + Ship (milestone release v0.4.5)
|
||||
|
||||
- **Task 5-1-01** (lead-developer → ci-review personas): multi-persona review of all v0.5 changes (correctness, testing, security, performance, maintainability) — P0s fixed in-phase.
|
||||
- **Task 5-2-01** (ci-audit): reconstruction test (git log ↔ .ciagent/ files), file/branch/commit discipline, tag hygiene; critical fixes in-phase.
|
||||
- **Task 5-3-01** (lead-developer → ci-ship): merge phase/05 → milestone → main; tag **v0.4.5** (milestone release); Gitea release with full summary + `nextcraft-linux-x64` + `.sha256` assets; delete milestone branches; mark requirements complete; clear checkpoint.
|
||||
|
||||
---
|
||||
|
||||
## Must-Haves (Milestone)
|
||||
|
||||
- **MH-M1**: Mid-burst disconnect loses zero events outside the acked margin (P1 regression test, repeatable).
|
||||
- **MH-M2**: `openai-audio` provider passes byte-contract STT/TTS tests with key-redaction pins; defense flow works server-side end-to-end (manual probe documented); mock/browser paths unchanged.
|
||||
- **MH-M3**: Identity flow (submit → pending → verified) works under mock; gates enforce 16+/18+ in the binding composition order with verify-CTA payloads; PII never stored raw or logged (caplog sentinel green).
|
||||
- **MH-M4**: Design + simulation environment kinds provisionable with per-kind starter contents + exec policy; grading digest proven kind-agnostic.
|
||||
- **MH-M5**: All gates green at every phase ship: `pnpm ai:test`, ruff, `pnpm typecheck`, `pnpm build`, web + cli tests; binary assets on every release (D-036).
|
||||
- **MH-M6**: v0.1 surface regressions: zero **for verified allowlisted pilot learners** (existing learner/marketplace/employer/admin flows unchanged except the additive enroll route + badges/CTAs; gating unverified/under-age ids is REQ-5-004's purpose, not a regression).
|
||||
|
||||
**Out of scope (guarded):** real KYC vendor integration (protocol only), real auth sessions (self-asserted learner_id documented as pilot-scale), marketplace backend beyond the one gated stub route, new isolation tech (D-024 unchanged), server-side starter materialization (documented as future seam).
|
||||
1. Board shows real postings with an honest empty state (no fake cards when the store is empty).
|
||||
2. Missing posting id → honest 404 page (no fabricated job).
|
||||
3. Unverified learner clicking Apply → verify-CTA prompt linking to enrollment (never a dead JSON error or a fake "applied").
|
||||
4. Match scores are computed cosine values only; no mock `matchScore` anywhere; absent score = absent query context (honest).
|
||||
5. Tag facets derive from real stored tags (API payload → sidebar), never hardcoded lists.
|
||||
6. Per-field provenance markers visible on the employer profile (raw/llm/manual).
|
||||
7. Sitemap lists published postings; robots points at the sitemap; metadata present in the exported HTML.
|
||||
8. Resume auto-fill never persists without an explicit confirm; oversize/wrong-format/text-less PDFs get honest, actionable errors.
|
||||
9. All existing accessibility baselines hold (WCAG AA on new states).
|
||||
+38
-1
@@ -16,7 +16,26 @@ Nextcraft is an AI-native outcome school where graduates prove what they can bui
|
||||
|
||||
---
|
||||
|
||||
## Milestone v0.5 — Real Server Voice, KYC/Identity, Sandbox Environments, Seq-Lease (COMPLETE, shipped as v0.4.5)
|
||||
## Milestone v0.6 — Marketplace Engine (ACTIVE)
|
||||
|
||||
**Scope (auto-decided at SPECIFY per full autonomy — founder scope question left unanswered 2026-09-21):**
|
||||
|
||||
The marketplace has no backend today: every job board, job detail, employer profile, search, and pricing surface reads `packages/mock-data` directly; the only live route is the G-18 `POST /v1/marketplace/apply` gated stub (18+ verified, honest 501). The guiding outcome — ≥1,000 learners placed in AI-orchestration roles via the Nextcraft marketplace — runs *through* this surface, so the marketplace engine is the highest-leverage next build.
|
||||
|
||||
**Scope decision (D-047):** Activate **REQ-F-011..016** as REQ-6-001..006 — the real marketplace backend behind the existing UI:
|
||||
|
||||
1. **Job aggregation pipeline** (REQ-6-001, from REQ-F-011): job ingestion from employer postings + a seeded/synthetic corpus (3M+ external jobs is post-MVP — the pipeline ingests at pilot scale with the same adapter contract); postings store (6th D-027 store family); posting lifecycle (draft → published → closed).
|
||||
2. **AI-powered tagging** (REQ-6-002, from REQ-F-012): skills/seniority/category extraction from posting text via the existing LLM structured-output layer (D-020), provider-agnostic, mock-first.
|
||||
3. **Semantic vector search** (REQ-6-003, from REQ-F-013): embedding-backed role matching — candidate-competency vs posting-requirement similarity; SQLite-local at pilot scale (no pgvector before Postgres migration), deterministic embedding provider protocol (mock-first, real provider env-configured).
|
||||
4. **Company enrichment** (REQ-6-004, from REQ-F-014): employer profile enrichment from posting/company data (descriptions, social links, logo refs) — LLM-assisted, human-in-the-loop founder review deferred to post-pilot.
|
||||
5. **Resume/profile parsing** (REQ-6-005, from REQ-F-015): learner profile auto-fill from an uploaded resume — LLM structured extraction mapping to the competency profile shape.
|
||||
6. **SEO-optimized programmatic pages** (REQ-6-006, from REQ-F-016): server-rendered job/stack/landing pages with metadata, sitemap, structured data — built on the existing Next.js static-export single-port deploy (D-039-compatible).
|
||||
|
||||
**Deferred beyond v0.6 (unchanged):** REQ-F-018 payments/subscriptions, REQ-F-019 human tutor marketplace, REQ-F-020 CIRR placement tracking.
|
||||
|
||||
**Success (milestone-level):** the marketplace surfaces read from a real backend (postings, search, matching, employer profiles) instead of mock data; job posting + tagging + search work end-to-end for a pilot employer; the deploy stays one-command on the v0.4.x/v0.5.x fabric.
|
||||
|
||||
## Prior Milestone: v0.5 — Real Server Voice, KYC/Identity, Sandbox Environments, Seq-Lease (COMPLETE, shipped as v0.4.5; hotfixes v0.4.6 deploy-static-404, v0.4.7 pre-public hardening)
|
||||
|
||||
**Scope (the four seams D-016 deferred out of v0.4, locked at v0.5 Phase 0 SPECIFY):**
|
||||
|
||||
@@ -59,6 +78,23 @@ All 8 v0.3 requirements (REQ-3-001..008) are complete and shipped as v0.2.8. See
|
||||
|
||||
All 28 v0.1 requirements (REQ-001..028) are complete and shipped as v0.1.0. See REQUIREMENTS.md traceability matrix.
|
||||
|
||||
## Clarified Assumptions (v0.6 CLARIFY stage, full autonomy — auto-resolved)
|
||||
|
||||
| # | Ambiguity | Resolution | Confidence |
|
||||
|---|-----------|------------|-------------|
|
||||
| A-401 | Who authors postings at pilot (no employer auth exists)? | **Founder-authored; employer self-serve deferred.** Postings enter via founder/admin API routes (`/v1/marketplace/admin/postings` create + lifecycle transitions draft→published→closed), guarded by an `AI_ADMIN_TOKEN` env-header check (keys env-only, D-014; operator route — D-043 learner gate composition untouched). The employer dashboard's posting-management mockup stays mock; real employer self-serve lands with the deferred B2B employer network milestone. | 0.85 |
|
||||
| A-402 | Ingestion adapter contract shape? | **`PostingSource` protocol in `ai_service/marketplace/`** (D-031 module, D-014 mock-first): `fetch() -> Iterable[IngestPosting]` — pydantic-typed with `source`, `external_id`, employer + posting fields. Two v0.6 implementations: `seeded_corpus` (in-repo JSON seed, ported once from packages/mock-data jobs.ts/employers.ts per D-021 convention) and `admin_api` (A-401 founder path). Upserts idempotent on `(source, external_id)`. No scheduler/crawler — the 3M+ external corpus drops in later as a new adapter with zero store/route changes. | 0.80 |
|
||||
| A-403 | Where do embeddings live at SQLite scale (pip-only venv, no pgvector)? | **`embedding` table inside the marketplace SQLite store (6th D-027 family)**: posting_id → BLOB of struct-packed float32 + model + dim columns; the candidate side embeds the learner's competency profile text on demand (not stored). **Brute-force cosine in pure Python** at query time (pilot scale ≤ ~10⁴ postings — no numpy, no FAISS/chromadb, no new deps). `VectorStore` protocol-wrapped so the Postgres migration swaps to pgvector with no API changes (D-027/D-019 pattern). | 0.85 |
|
||||
| A-404 | Embedding provider + dimensionality? | **`EmbeddingProvider` protocol (mirror of D-030 VoiceProvider)**: deterministic mock (256-dim feature-hash of posting/profile tokens — similarity is deterministic; tests never call the cloud) + real OpenAI-compatible `POST {AI_EMBED_BASE_URL}/embeddings` via `AI_EMBED_MODEL`/`AI_EMBED_API_KEY` (env-only; ollama-cloud exposes /embeddings). Dimension is **not hardcoded** — persisted per row with the model name; on provider/model change, postings re-embed lazily. **[SUPERSEDED by D-051 probe: ollama-cloud does NOT expose /embeddings (404 path-not-found) — `AI_EMBED_BASE_URL` must target another OpenAI-compatible endpoint (e.g. local ollama `:11434/v1`); protocol design unaffected, default stays mock]** | 0.75 |
|
||||
| A-405 | Does semantic search replace the filter sidebar? | **Hybrid — augments, not replaces.** REQ-6-002 tag facets (skills/seniority/category + location/remote/salary) remain hard filters driving the existing sidebar; the semantic query + similarity compose as a **ranking layer over the facet-filtered set** (no facets selected → pure semantic ranking). Match score on job cards = cosine(candidate-competency vector, posting vector), replacing the mock `matchScore`; skill-breakdown panel derives from tag overlap + per-skill vector contribution. ARCHITECTURE.md's old "filter UI replaced" future-note is superseded by this hybrid. | 0.80 |
|
||||
| A-406 | What happens to packages/mock-data jobs.ts/employers.ts (runtime data vs static export)? | **Marketplace web surfaces switch from build-time mock imports to runtime client fetch** (A-002 direct-fetch + engine-client pattern, proven by build/defend surfaces): board/detail/employer-profile become client shells fetching `/v1/marketplace/*`; `generateStaticParams` on jobs/[jobId] and employers/[employerId] is removed (one static shell per route, data at runtime). jobs.ts/employers.ts are ported **once** into the Python seed fixture (A-402) and go dormant for the marketplace path (mock-data remains for admin/employer-dashboard mockups + shared types). No build-time DB reads — the D-039 deploy export build stays repo-only. | 0.80 |
|
||||
| A-407 | Employer auth model at pilot? | **None — employers are data, not principals, in v0.6.** Employer records are ingested/enriched entities keyed by `employer_id` on postings; no employer login, sessions, or accounts. The employer dashboard surface stays the v0.1 mockup; enrichment is founder-triggered/admin-reviewed (A-409), never employer-edited. Real employer auth lands post-payments with the B2B employer network. | 0.85 |
|
||||
| A-408 | Apply-flow shape beyond the 501 stub? | **The existing `POST /v1/marketplace/apply` route becomes real, D-043 composition unchanged** (G-5 allowlist → verified-18+ verdict → rate caps): body `{posting_id}` → posting must be `published` → insert an `application` row (status `submitted`, identity ref, timestamp) in the marketplace store → honest confirmation payload. Duplicate apply (same learner + posting) is idempotent. **No notification infra**: founder reviews applications via `/v1/marketplace/admin/applications` and contacts employers out-of-band. | 0.75 |
|
||||
| A-409 | Enrichment provenance rules? | **Separate `employer_enrichment` table — raw fields never overwritten** (REQ-6-004): append-only rows `(employer_id, field, value, source: llm\|manual, model, created_at)`. Read precedence **manual > llm > ingested-raw**; API responses merge and carry per-field provenance markers (A-304 honesty pattern — LLM-authored copy visibly marked). Enrich-on-post: new employer's first published posting triggers LLM structured extraction (D-020, mock-first) of description/social links/logo refs. Founder override = upsert a `manual`-source row, never a mutation of `llm` rows. | 0.85 |
|
||||
| A-410 | Resume upload format bounds? | **PDF/TXT/MD only, ≤2 MB, text-layer required.** PDF text via `pypdf` (pure-Python, pip-reachable through the A-009 venv-pip channel — the only new dep); extracted text capped ~100K chars before the D-020 structured-extraction call mapping to the competency-profile shape. Image-only/scanned PDFs → honest 422 ("text layer required") — **no OCR in v0.6**. Parsed result returns as a DRAFT: nothing persists to the learner profile until explicit learner confirm (REQ-6-005 review gate); raw resume bytes never stored, never logged (A-305 pattern). | 0.80 |
|
||||
| A-411 | Which pages get programmatic SEO treatment under static export — and how? | **Build-time-static set: landing, catalog/stack detail, marketplace board, pricing** — full server-component metadata (title/desc/OG) + JSON-LD (Organization, stack/Course, ItemList) rendered at export (D-039 build unchanged). **Runtime-data set: job detail + employer profile** — client shells (A-406) inject `JobPosting`/`Organization` JSON-LD + title client-side after fetch (best-effort per-job SEO at pilot; recorded as an accepted limitation — JS-less crawlers see the shell). **Sitemap/robots: `/sitemap.xml` + `/robots.txt` served dynamically by ai-service** (API routes beat the static mount, D-039), enumerating static pages + published postings; robots points at `/sitemap.xml`. | 0.70 |
|
||||
| A-412 | Pricing page — real or mock? | **Stays mock — zero backend.** Payments/subscriptions are REQ-F-018, explicitly deferred beyond v0.6 (D-047). The pricing page gains only build-time metadata/OG (A-411 static set); no pricing API routes, no package enforcement — posting is free at pilot (founder-mediated). | 0.95 |
|
||||
|
||||
## Clarified Assumptions (v0.5 CLARIFY stage, full autonomy — auto-resolved)
|
||||
|
||||
| # | Ambiguity | Resolution | Confidence |
|
||||
@@ -183,6 +219,7 @@ The following remain deferred beyond v0.3 and will be activated in subsequent mi
|
||||
| D-014 | Provider-agnostic LLM layer; ollama-cloud as initial provider | OpenAI-compatible client abstraction with pluggable providers: ollama-cloud (https://ollama.com/v1, default), local OpenAI-compatible endpoint, deterministic mock (tests/CI). Keys in gitignored .ciagent/.env.secrets, never in code or commits. | apps/ai-service llm package with 3 providers; default=ollama-cloud |
|
||||
| D-015 | All six agents implemented as real LLM services; engines mocked | Coach/Tutor/Mentor fully real. Lab/Assessor/Proctor are real LLM logic over mock inputs (simulated telemetry, pre-baked artifacts) since sandbox fabric, assessment engine, and identity verification are v0.3+. Consistent with v0.1's mock-data approach. | REQ-F-001..006 complete in v0.2; real engines deferred to v0.3+ |
|
||||
| D-016 | v0.4 = Distribution & Bootstrap CLI (founder directive supersedes previously-named v0.4 seams) | Founder directive 2026-09-12: focus this milestone on streamlining install, a bootstrap CLI with a one-liner install script, ongoing release binaries. Real server STT/TTS, KYC, design/simulation envs, seq-lease move to v0.5. | Milestone scope locked at SPECIFY; binary = CLI-only, linux x64 |
|
||||
| D-047 | v0.6 = Marketplace Engine (auto-decided at SPECIFY, full autonomy; founder scope question left unanswered 2026-09-21) | Activates REQ-F-011..016 as REQ-6-001..006: real marketplace backend (aggregation, tagging, vector search, enrichment, resume parsing, SEO pages). Rationale: the guiding outcome (≥1,000 placements via the marketplace) runs through the only surface with zero backend; every prior milestone fed the credential side, placement side is the bottleneck. Confidence 0.80 ≥ threshold 0.60 (D-007). External 3M+ job corpus scaled to pilot (adapter contract preserves the full-scale path). | Milestone scope locked at SPECIFY; tags on v0.5.x line; feature milestone |
|
||||
| D-039 | Single-port deploy + unattended dev (v0.3.6 hotfix, founder directive) | Only :8420 reachable behind HAProxy; site was down (domain root hit the API 404). Web app = static export served same-origin by the ai-service (`AI_WEB_STATIC_DIR`); `dev -d`/`stop`/`log` daemonize ops; durable state (DB, sandboxes, pid/log) moves to `~/.nextcraft/` out of the repo. | One port serves UI+API; unattended deploy recipe in deploy/README.md |
|
||||
|
||||
---
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
# Nextcraft — REQUIREMENTS.md
|
||||
|
||||
## v0.6 Requirements (ACTIVE — Marketplace Engine, milestone v0.6)
|
||||
|
||||
### Job Aggregation & Postings
|
||||
|
||||
| ID | Description | Priority | Phase | Status |
|
||||
|----|-------------|----------|-------|--------|
|
||||
| REQ-6-001 | Job posting backend (from REQ-F-011, pilot-scaled): postings store (6th D-027 store family — SQLite, insert-safe, protocol-wrapped), employer posting lifecycle (draft → published → closed), ingestion adapter contract (employer-authored postings + seeded corpus at pilot scale; 3M+ external aggregation preserved by the adapter interface), marketplace API routes replacing the mock-data reads on job board/detail/employer surfaces, 18+ verified identity gate on apply/post flows (D-043 composition retained) | critical | 1 | pending |
|
||||
| REQ-6-002 | AI-powered tagging (from REQ-F-012): skills/seniority/category extraction from posting text via the LLM structured-output layer (D-020) — deterministic mock-first, provider-agnostic; tags persisted with the posting; tag facets drive the search filter sidebar | high | 2 | pending |
|
||||
|
||||
### Search & Matching
|
||||
|
||||
| ID | Description | Priority | Phase | Status |
|
||||
|----|-------------|----------|-------|--------|
|
||||
| REQ-6-003 | Semantic vector search (from REQ-F-013): embedding provider protocol (mock-first, deterministic; real provider env-configured per D-014), vector store at pilot scale (SQLite-local — no pgvector before the Postgres migration), candidate-competency ↔ posting-requirement similarity ranking, match-score surfacing on job cards + candidate matches (replaces mock match scores) | critical | 3 | pending |
|
||||
|
||||
### Enrichment & Parsing
|
||||
|
||||
| ID | Description | Priority | Phase | Status |
|
||||
|----|-------------|----------|-------|--------|
|
||||
| REQ-6-004 | Company enrichment (from REQ-F-014): employer profile enrichment from posting/company data (descriptions, social links, logo refs) via LLM structured output; enrich-on-post with manual founder override path; enriched data stored separately from raw (provenance marked) | medium | 4 | pending |
|
||||
| REQ-6-005 | Resume/profile parsing (from REQ-F-015): learner profile auto-fill from an uploaded resume — LLM structured extraction mapping to the competency-profile shape, learner-review-before-save (never silently overwrite), upload bounded (size/format guard, house pattern) | medium | 4 | pending |
|
||||
|
||||
### SEO & Programmatic Pages
|
||||
|
||||
| ID | Description | Priority | Phase | Status |
|
||||
|----|-------------|----------|-------|--------|
|
||||
| REQ-6-006 | SEO-optimized programmatic pages (from REQ-F-016): server-rendered job/stack/landing pages with metadata (title/description/OG), sitemap.xml + robots.txt, JSON-LD structured data (JobPosting schema), static-export compatible (D-039 single-port deploy unchanged) | high | 5 | pending |
|
||||
|
||||
## v0.5 Requirements (Complete — Real Server Voice, KYC/Identity, Sandbox Environments, Seq-Lease; shipped as v0.4.5)
|
||||
|
||||
### Real Server Voice
|
||||
@@ -182,12 +210,12 @@
|
||||
|
||||
| ID | Description | Priority | Milestone | Status |
|
||||
|----|-------------|----------|-----------|--------|
|
||||
| REQ-F-011 | AI job aggregation pipeline (3M+ jobs from 120K companies) | high | v0.4+ | deferred |
|
||||
| REQ-F-012 | AI-powered tagging (skills, categories, seniority) | high | v0.4+ | deferred |
|
||||
| REQ-F-013 | Semantic vector search for role matching | high | v0.4+ | deferred |
|
||||
| REQ-F-014 | AI company enrichment (logos, descriptions, social links) | medium | v0.4+ | deferred |
|
||||
| REQ-F-015 | AI resume parsing (profile auto-fill) | medium | v0.4+ | deferred |
|
||||
| REQ-F-016 | SEO-optimized programmatic pages | medium | v0.4+ | deferred |
|
||||
| REQ-F-011 | AI job aggregation pipeline (3M+ jobs from 120K companies) | high | v0.6 | activated → REQ-6-001 (pilot-scaled; adapter contract preserves full scale) |
|
||||
| REQ-F-012 | AI-powered tagging (skills, categories, seniority) | high | v0.6 | activated → REQ-6-002 |
|
||||
| REQ-F-013 | Semantic vector search for role matching | high | v0.6 | activated → REQ-6-003 |
|
||||
| REQ-F-014 | AI company enrichment (logos, descriptions, social links) | medium | v0.6 | activated → REQ-6-004 |
|
||||
| REQ-F-015 | AI resume parsing (profile auto-fill) | medium | v0.6 | activated → REQ-6-005 |
|
||||
| REQ-F-016 | SEO-optimized programmatic pages | medium | v0.6 | activated → REQ-6-006 |
|
||||
|
||||
### Platform Infrastructure
|
||||
|
||||
@@ -216,6 +244,17 @@
|
||||
|
||||
## Traceability Matrix
|
||||
|
||||
### v0.6 (active)
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|-------------|-------|--------|
|
||||
| REQ-6-001 | 1 | pending |
|
||||
| REQ-6-002 | 2 | pending |
|
||||
| REQ-6-003 | 3 | pending |
|
||||
| REQ-6-004 | 4 | pending |
|
||||
| REQ-6-005 | 4 | pending |
|
||||
| REQ-6-006 | 5 | pending |
|
||||
|
||||
### v0.5 (complete)
|
||||
|
||||
| Requirement | Phase | Status |
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## Overview
|
||||
|
||||
**Milestone v0.6 — Marketplace Engine (ACTIVE, started 2026-09-21).** The real marketplace backend behind the existing surfaces: job posting backend + aggregation adapter (REQ-6-001), AI tagging (REQ-6-002), semantic vector search + match scoring (REQ-6-003), company enrichment + resume parsing (REQ-6-004/005), SEO programmatic pages (REQ-6-006). Scope decision D-047 (auto-decided at SPECIFY, full autonomy). Tags on the v0.5.x line (v0.5.0 phase 0 → v0.5.N final = milestone release); branch `milestone/v0.6-marketplace-engine`.
|
||||
|
||||
**Hotfix v0.4.7 (2026-09-21) — pre-public hardening.** The 4 P1 flags from the v0.5 final review closed: HMAC ingest tokens on the WS grading input (LAN poisoning closed), nonce+counter defense-id mint (same-ms collision closed), browser-mode client SR/TTS per the descriptor contract (gap closed), 8 browser-voice module pins. Deployed via `nextcraft deploy --service`; site 200, service enabled.
|
||||
|
||||
**Hotfix v0.4.6 (2026-09-21) — site-down + deploy experience.** The domain root (nextcraft.coreci.dev) served the API's JSON 404 for three releases: the deployed service ran mount-less (web export never built on the host; dev -d auto-wiring silently skipped; nothing detected it). Fixed the class: `nextcraft deploy` (one-command clone/update → install → venv → export build → restart → verify), `deploy --service` (generated systemd unit, reboot-persistent), toolchain discovery (nvm/PNPM_HOME/corepack, incl. split installs — the host's exact layout), loud mount-less boot warning, verify live mount probe. Deployed live on the host via the new command; site 200, service enabled.
|
||||
|
||||
**Hotfix v0.3.5/v0.3.6 (2026-09-13) — fresh-box + single-port unattended deploy.**
|
||||
@@ -24,6 +28,22 @@
|
||||
|
||||
## Phase List
|
||||
|
||||
### v0.6 (active)
|
||||
|
||||
| # | Name | Status | Depends On | Requirements | Success Criteria |
|
||||
|---|------|--------|------------|--------------|------------------|
|
||||
| 0 | Pre-execution | in-progress | — | — | Specification, clarify, research, plan, grill, MVP/UX check complete; .ciagent/ files updated for v0.6 |
|
||||
| 1 | Postings backend + aggregation adapter | pending | 0 | REQ-6-001 | Postings store + lifecycle + `/v1/marketplace/*` routes live; job board/detail/employer surfaces read real data; 18+ gates composed per D-043; seeded ingestion adapter proven |
|
||||
| 2 | AI tagging | pending | 1 | REQ-6-002 | Posting text → skills/seniority/category tags via LLM structured output (mock-first); tag facets drive search filters; tags persisted |
|
||||
| 3 | Semantic vector search | pending | 2 | REQ-6-003 | Embedding provider protocol (mock-first) + SQLite-local vectors; competency↔posting similarity ranking; match scores surface on job cards + candidate matches |
|
||||
| 4 | Enrichment + resume parsing | pending | 3 | REQ-6-004, REQ-6-005 | Employer enrichment from posting data with provenance; resume upload → profile auto-fill with learner review gate; bounded uploads |
|
||||
| 5 | SEO programmatic pages | pending | 4 | REQ-6-006 | Metadata + JSON-LD + sitemap/robots on job/stack/landing pages; static-export compatible (single-port deploy unchanged) |
|
||||
| 6 | Final review + ship | pending | 1-5 | — | Review clean; audit passes; milestone tagged (final v0.5.x patch); release with binary assets on Gitea; milestone merged to main |
|
||||
|
||||
---
|
||||
|
||||
## Phase List (v0.5 — complete)
|
||||
|
||||
| # | Name | Status | Depends On | Requirements | Success Criteria |
|
||||
|---|------|--------|------------|--------------|------------------|
|
||||
| 0 | Pre-execution | complete | — | — | Specification, clarify, research, plan, grill complete; .ciagent/ files updated for v0.5 |
|
||||
@@ -106,6 +126,10 @@
|
||||
| v0.4.7 | 2026-09-21 | Pre-public hardening (the 4 P1 flags from the v0.5 final review): HMAC ingest tokens on the WS grading input (LAN trace-poisoning closed, per-boot secret, manager mints / endpoint verifies), nonce+counter defense-id mint (same-ms PK collision closed), browser-mode client SR/TTS implemented per the descriptor contract (the v0.5 gap), 8 browser-voice module pins. 488 backend (+10), web 22/22 (+8), CLI 61/61. Deployed via `nextcraft deploy --service`; site 200, service enabled. |
|
||||
| v0.4.6 | 2026-09-21 | Site-down hotfix + one-command deploy: `nextcraft deploy [--service]` (clone/update → install → venv → export build → restart → verify), systemd unit generation (reboot persistence), toolchain discovery (nvm/PNPM_HOME/corepack + split installs), loud mount-less boot, verify live mount probe, subcommand flag dispatch. Deployed live via the command itself; nextcraft.coreci.dev → 200 HTML. |
|
||||
|
||||
## v0.6 (Active — Marketplace Engine)
|
||||
|
||||
Real backend behind the marketplace surfaces: postings store + aggregation adapter + marketplace API (REQ-6-001), AI tagging (REQ-6-002), embedding-backed semantic search + match scores (REQ-6-003), company enrichment + resume parsing (REQ-6-004/005), SEO programmatic pages (REQ-6-006). 7 phases (P0–P6). Scope D-047 (auto-decided at SPECIFY). Tags v0.5.0+ on the v0.5.x line; final patch = milestone release.
|
||||
|
||||
## v0.5 (Complete — Shipped as v0.4.5)
|
||||
|
||||
Real Server Voice, KYC/Identity, Sandbox Environments, Seq-Lease: real server STT/TTS (`openai-audio` VoiceProvider with boot-safe fallback), the identity module (5th D-027 store, provider protocol + mock, D-043 age-gate composition on variants/sandboxes/defense/marketplace), design/simulation environment kinds at the template layer with per-kind exec policy, and the seq-ack protocol closing the P07 replay-margin gap. 6 phases (P0–P5). All 7 requirements (REQ-5-001..007) complete. Tags v0.4.0–v0.4.4 per phase, milestone release v0.4.5.
|
||||
|
||||
@@ -46,9 +46,9 @@
|
||||
"projects": [],
|
||||
"active_project": null,
|
||||
"milestone": {
|
||||
"version": "v0.5",
|
||||
"name": "real-voice-identity-envs",
|
||||
"version": "v0.6",
|
||||
"name": "marketplace-engine",
|
||||
"type": "feature",
|
||||
"branch": "milestone/v0.5-real-voice-identity-envs"
|
||||
"branch": "milestone/v0.6-marketplace-engine"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user