From 3db7e4799456da9d11700a834d509a7e203c7768 Mon Sep 17 00:00:00 2001 From: Hermes Date: Thu, 28 May 2026 20:26:22 +0200 Subject: [PATCH] docs: add constitution, spec, plan, and data model for InteriorScan MVP - Constitution: self-hosted first, FPS-grade immersion, mobile-first capture, API-first, open formats - Spec: 6 user stories (upload & process, share & embed, dashboard, API, measurements, billing) - Plan: 6 phases, 50 tasks, Phase 1-3 = MVP core - Data model: Drizzle schema with projects, photos, walkthroughs, processing_jobs, subscriptions --- .specify/memory/constitution.md | 46 ++++ .specify/specs/001-interiorscan-mvp/SPEC.md | 121 ++++++++++ .../specs/001-interiorscan-mvp/data-model.md | 207 ++++++++++++++++++ .specify/specs/001-interiorscan-mvp/plan.md | 131 +++++++++++ AGENTS.md | 71 ++++++ README.md | 60 ++++- 6 files changed, 634 insertions(+), 2 deletions(-) create mode 100644 .specify/memory/constitution.md create mode 100644 .specify/specs/001-interiorscan-mvp/SPEC.md create mode 100644 .specify/specs/001-interiorscan-mvp/data-model.md create mode 100644 .specify/specs/001-interiorscan-mvp/plan.md create mode 100644 AGENTS.md diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md new file mode 100644 index 0000000..223b53c --- /dev/null +++ b/.specify/memory/constitution.md @@ -0,0 +1,46 @@ +# InteriorScan Constitution + +## Core Principles + +### I. Self-Hosted First (NON-NEGOTIABLE) +All infrastructure must be self-hosted or FOSS. The ONLY external SaaS dependency is Stripe for billing. Database: PostgreSQL. Cache/sessions: Valkey. Processing: own GPU nodes. No vendor lock-in on reconstruction pipeline, viewer, or storage. + +### II. Progressive Experience +Every feature must degrade gracefully. If WebGL fails, show 360° fallback. If processing fails, show uploaded photos. No feature should have a single point of failure that blocks the entire experience. + +### III. FPS-Grade Immersion +The 3D viewer is not a "virtual tour" — it is a navigable space. Controls feel like a first-person game: WASD + mouse on desktop, tap-to-walk on mobile. Smooth movement, collision detection, gravity, head-bob optional. If it feels like a slideshow, it's wrong. + +### IV. Photo-First Capture +Mobile phone is the only required hardware. iPhone LiDAR and 360° cameras are enhancements, not prerequisites. A real estate agent with an Android phone must be able to capture and publish a walkthrough in under 30 minutes. + +### V. API-First Architecture +Every feature exposed via REST API before UI. InteriorScan is a reconstruction-as-a-service platform that happens to have a web UI, not a walled garden. White-label and embeddable viewers are first-class use cases. + +### VI. TypeScript Strict +`strict: true` + `noUncheckedIndexedAccess: true`. No `any` without explicit justification. Zod for runtime validation. neverthrow for error handling in business logic. + +### VII. Test-First (NON-NEGOTIABLE) +TDD mandatory: write failing test → user approves test → verify test fails → implement → verify test passes. No untested API endpoints, no untested business logic. + +### VIII. Open Formats +All reconstruction outputs (mesh, point cloud, floor plan) must be exportable in open formats: OBJ, E57, LAS/LAZ, DXF, IFC (P4+). Proprietary lock-in is antithetical to this project's mission. + +## Additional Constraints + +- **Stack**: Next.js 16 App Router, Drizzle ORM, PostgreSQL, Valkey, three.js +- **Monorepo**: Turborepo + pnpm (tpl-next-saas template) +- **Billing**: Stripe only — no other payment/subscription SaaS +- **Lint/Format**: Biome 2.x ONLY — zero ESLint, zero Prettier +- **Testing**: Vitest v4 + Playwright +- **Design**: WCAG 2.2 AA minimum, d0a1 design palettes +- **Language**: Code/docs/commits in English. Team speaks Spanish. + +## Governance + +- Constitution supersedes all other practices +- Amendments require documentation, approval, and migration plan +- All PRs/reviews must verify compliance +- Retro-propagation: patterns validated in production must be PR'd back to `agentic-standards` + +**Version**: 1.0.0 | **Ratified**: 2026-05-28 | **Last Amended**: 2026-05-28 \ No newline at end of file diff --git a/.specify/specs/001-interiorscan-mvp/SPEC.md b/.specify/specs/001-interiorscan-mvp/SPEC.md new file mode 100644 index 0000000..b98ff1d --- /dev/null +++ b/.specify/specs/001-interiorscan-mvp/SPEC.md @@ -0,0 +1,121 @@ +# Feature Specification: InteriorScan MVP +**Feature Branch**: `feat/001-interiorscan-mvp` +**Status**: Draft — Awaiting Approval + +## Vision Statement + +**InteriorScan turns apartment photos into a navigable 3D space** — like a Doom map of a real home. Real estate agents photograph a property, upload to InteriorScan, and share a link where potential buyers walk through the space in first-person. This filters out properties that don't fit, saving both agents and buyers from unnecessary physical visits. + +## User Scenarios & Testing + +### User Story 1 — Upload & Process (Priority: P1) 🎯 MVP Core + +As a real estate agent, I upload 30-100 photos of a property and InteriorScan reconstructs the interior as a 3D walkthrough, so I can share a link with potential buyers who navigate it like a first-person game. + +**Why this priority**: Without reconstruction, nothing else matters. This is the core value proposition. + +**Independent Test**: Upload photos → receive a shareable walkthrough URL. A visitor opens URL → navigates space in first-person. + +**Acceptance Scenarios**: +1. **Given** I am logged in, **When** I create a new project, upload 40+ JPEG photos of an interior, and submit for processing, **Then** the system accepts the upload, shows a processing status (queued → processing → complete), and within 60 minutes produces a 3D walkthrough. +2. **Given** a walkthrough has completed processing, **When** I open the shareable link in a desktop browser, **Then** I see the reconstructed 3D space with WASD + mouse controls, collision detection, and texture-mapped walls/floors/ceilings. +3. **Given** a walkthrough has completed processing, **When** I open the shareable link on a mobile browser, **Then** I see the reconstructed 3D space with tap-to-walk navigation and gyroscope look-around option. + +### User Story 2 — Share & Embed (Priority: P1) 🎯 MVP Distribution + +As a real estate agent, I can share a walkthrough link or embed the viewer on any listing page, so potential buyers encounter the 3D tour wherever they find the property. + +**Why this priority**: The walkthrough is useless without distribution. Sharing and embedding are how real estate works. + +**Independent Test**: Agent shares link → opens in new tab. Agent copies embed code → pastes on WordPress/listing → viewer renders. + +**Acceptance Scenarios**: +1. **Given** a completed walkthrough, **When** I click "Share", **Then** I get a copyable link and an oEmbed-embeddable iframe snippet. +2. **Given** an embed snippet, **When** I paste it into a website, **Then** the InteriorScan viewer renders inline with all navigation controls functional. +3. **Given** a shared link, **When** an unauthenticated visitor opens it, **Then** they can navigate the full walkthrough without signing up (public access). + +### User Story 3 — Dashboard & Projects (Priority: P2) + +As a real estate agent, I manage all my properties from a dashboard, so I can track processing status, edit metadata, and organize my portfolio. + +**Why this priority**: Essential for retention but not for first-value. A single walkthrough proves the concept. + +**Independent Test**: Agent creates 3 projects → sees them in dashboard → edits name/address → deletes one. + +**Acceptance Scenarios**: +1. **Given** I am logged in, **When** I visit the dashboard, **Then** I see a grid of my projects with thumbnail, name, address, processing status, and date. +2. **Given** a project exists, **When** I edit its name or address, **Then** the changes persist and appear in the walkthrough metadata. +3. **Given** I have 3 projects, **When** I delete one, **Then** it is removed from the dashboard and its data is scheduled for cleanup within 24h. + +### User Story 4 — API Access (Priority: P2) + +As a developer integrating InteriorScan, I can upload photos and retrieve walkthroughs via REST API, so I can embed reconstruction into my own platform. + +**Why this priority**: API-first is a constitution principle, but the web UI validates the pipeline first. API follows proven flow. + +**Independent Test**: `POST /api/v1/projects` with photos → `GET /api/v1/projects/:id` returns status → `GET /api/v1/projects/:id/walkthrough` returns viewer URL. + +**Acceptance Scenarios**: +1. **Given** I have an API key, **When** I POST photos to `/api/v1/projects`, **Then** I receive a project ID and a `Location` header for status polling. +2. **Given** a project is processing, **When** I GET `/api/v1/projects/:id`, **Then** I receive `{status: "processing", progress: 45, estimatedCompletion: "..."}`. +3. **Given** a project is complete, **When** I GET `/api/v1/projects/:id/walkthrough`, **Then** I receive the viewer URL and embed snippet. + +### User Story 5 — Measurements & Floor Plans (Priority: P3) + +As a buyer, I can see room dimensions overlaid on the walkthrough and download a floor plan, so I can evaluate if furniture fits without visiting. + +**Why this priority**: Differentiates from pure 360° tours but requires accurate reconstruction. Comes after core pipeline is proven. + +**Independent Test**: Open walkthrough → toggle measurements → see room dimensions in meters. + +**Acceptance Scenarios**: +1. **Given** a walkthrough with LiDAR data, **When** I toggle "Show Measurements", **Then** room dimensions appear as overlays on walls and floor (±5% accuracy). +2. **Given** a completed walkthrough, **When** I click "Download Floor Plan", **Then** I receive a PDF/DXF floor plan with labeled rooms and dimensions. + +### User Story 6 — Subscription & Billing (Priority: P3) + +As an agent, I subscribe to a plan (Free/Pro/Enterprise) that determines how many walkthroughs I can create per month, so I only pay for what I use. + +**Why this priority**: Revenue model, but not needed to validate the core pipeline. + +**Acceptance Scenarios**: +1. **Given** I am on the Free plan, **When** I attempt to create my 4th walkthrough, **Then** I see an upgrade prompt with Pro plan pricing. +2. **Given** I upgrade to Pro, **When** payment is confirmed via Stripe, **Then** my walkthrough limit increases to 25/month and the badge updates. + +## Edge Cases + +- **Fewer than 20 photos**: System rejects with guidance on minimum coverage +- **Corrupt/unreadable photos**: System skips corrupt files, warns user, continues with valid ones +- **Processing failure**: System retries once, then marks as "failed" with error details and refund option +- **Very large spaces (>200 photos)**: System queues with estimated time, processes in chunks +- **Mobile browser without WebGL**: Fallback to 360° photo carousel (progressive enhancement) +- **Duplicate photos**: System deduplicates by perceptual hash before processing + +## Requirements + +### Functional Requirements +- **FR-001**: System MUST accept 20-500 JPEG/PNG photos per project via web upload or API +- **FR-002**: System MUST reconstruct 3D mesh from photos using COLMAP/OpenMVS pipeline +- **FR-003**: System MUST provide first-person navigation (WASD + mouse) in desktop browsers +- **FR-004**: System MUST provide tap-to-walk navigation in mobile browsers +- **FR-005**: System MUST generate shareable public links for completed walkthroughs +- **FR-006**: System MUST provide oEmbed-embeddable iframe code +- **FR-007**: System MUST show processing status: queued → processing → complete (with progress %) +- **FR-008**: System MUST handle processing failures gracefully with retry + notification +- **FR-009**: System MUST export walkthrough data in open formats (OBJ minimum) +- **FR-010**: System MUST work on iPhone, Android, desktop Chrome, Firefox, Safari + +### Non-Functional Requirements +- **NFR-001**: Walkthrough viewer MUST load initial mesh in <5 seconds on 4G +- **NFR-002**: Navigation MUST run at ≥30 FPS on mid-range 2024 devices +- **NFR-003**: Processing MUST complete within 60 minutes for 100 photos +- **NFR-004**: System MUST handle 10 concurrent processing jobs +- **NFR-005**: Walkthrough links MUST work without authentication (public) +- **NFR-006**: API MUST follow OpenAPI 3.1 spec with Zod-validated schemas + +## Success Criteria + +- **SC-001**: First tester uploads 40 photos → walks through reconstructed kitchen in <10 min total +- **SC-002**: Unauthenticated visitor opens shareable link → navigates fluently at 30+ FPS +- **SC-003**: Processing pipeline produces visual quality comparable to Matterport's basic tier +- **SC-004**: 3 of monyi's tester friends complete a walkthrough creation within 1 hour of onboarding \ No newline at end of file diff --git a/.specify/specs/001-interiorscan-mvp/data-model.md b/.specify/specs/001-interiorscan-mvp/data-model.md new file mode 100644 index 0000000..2f0cd51 --- /dev/null +++ b/.specify/specs/001-interiorscan-mvp/data-model.md @@ -0,0 +1,207 @@ +# InteriorScan — Data Model + +## Entity Relationship + +``` +users ──< team_members >── teams ──< projects ──< photos + │ │ + │ └── walkthroughs + │ + └── subscriptions + +projects ──< processing_jobs +``` + +## Drizzle Schema Definition + +```typescript +// packages/db/src/schema.ts + +import { pgTable, text, timestamp, integer, jsonb, boolean, pgEnum } from 'drizzle-orm/pg-core'; +import { cuid2 } from './custom-types'; // branded cuid2 ID type + +// ── Enums ────────────────────────────────────────── + +export const projectStatusEnum = pgEnum('project_status', [ + 'draft', // created, no photos yet + 'uploading', // photos being uploaded + 'queued', // queued for processing + 'processing', // COLMAP/OpenMVS pipeline running + 'complete', // walkthrough ready + 'failed', // processing error +]); + +export const processingStageEnum = pgEnum('processing_stage', [ + 'feature_extraction', // SIFT/ORB features + 'feature_matching', // Correspondence search + 'sfm', // Structure from Motion → sparse cloud + 'mvs', // Multi-View Stereo → dense cloud + 'meshing', // Mesh generation + 'texturing', // UV mapping + texturing + 'tiling', // LOD + Draco compression + 'complete', +]); + +export const subscriptionPlanEnum = pgEnum('subscription_plan', [ + 'free', + 'pro', + 'enterprise', +]); + +export const teamRoleEnum = pgEnum('team_role', [ + 'owner', + 'admin', + 'member', + 'viewer', +]); + +// ── Users (better-auth managed) ───────────────────── + +export const users = pgTable('users', { + id: cuid2('id').primaryKey(), + email: text('email').notNull().unique(), + name: text('name').notNull(), + emailVerified: boolean('email_verified').default(false), + image: text('image'), // avatar URL + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); + +// ── Teams & Membership ────────────────────────────── + +export const teams = pgTable('teams', { + id: cuid2('id').primaryKey(), + name: text('name').notNull(), + slug: text('slug').notNull().unique(), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); + +export const teamMembers = pgTable('team_members', { + id: cuid2('id').primaryKey(), + teamId: cuid2('team_id').references(() => teams.id).notNull(), + userId: cuid2('user_id').references(() => users.id).notNull(), + role: teamRoleEnum('role').default('member').notNull(), + createdAt: timestamp('created_at').defaultNow().notNull(), +}); + +// ── Projects ──────────────────────────────────────── + +export const projects = pgTable('projects', { + id: cuid2('id').primaryKey(), + teamId: cuid2('team_id').references(() => teams.id).notNull(), + name: text('name').notNull(), + address: text('address'), // property address + description: text('description'), + status: projectStatusEnum('status').default('draft').notNull(), + photoCount: integer('photo_count').default(0), + captureDevice: text('capture_device'), // 'iphone-lidar', 'android', 'theta-z1', 'unknown' + processingProgress: integer('processing_progress').default(0), // 0-100 + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); + +// ── Photos ────────────────────────────────────────── + +export const photos = pgTable('photos', { + id: cuid2('id').primaryKey(), + projectId: cuid2('project_id').references(() => projects.id, { onDelete: 'cascade' }).notNull(), + originalKey: text('original_key').notNull(), // S3/storage key for original + thumbnailKey: text('thumbnail_key'), // processed thumbnail key + perceptualHash: text('perceptual_hash'), // for deduplication + width: integer('width'), + height: integer('height'), + fileSizeBytes: integer('file_size_bytes'), + exifData: jsonb('exif_data'), // GPS, camera model, focal length + isDuplicate: boolean('is_duplicate').default(false), + createdAt: timestamp('created_at').defaultNow().notNull(), +}); + +// ── Walkthroughs ──────────────────────────────────── + +export const walkthroughs = pgTable('walkthroughs', { + id: cuid2('id').primaryKey(), + projectId: cuid2('project_id').references(() => projects.id).notNull(), + meshKey: text('mesh_key').notNull(), // tiled mesh storage key + textureKey: text('texture_key').notNull(), // texture atlas storage key + navmeshKey: text('navmesh_key'), // navmesh for click-to-walk + pointCloudKey: text('point_cloud_key'), // E57/LAS point cloud (optional) + thumbnailKey: text('thumbnail_key'), // preview image + bounds: jsonb('bounds'), // { min: [x,y,z], max: [x,y,z] } + stats: jsonb('stats'), // { vertices: N, faces: N, rooms: N } + isPublic: boolean('is_public').default(true), + slug: text('slug').notNull().unique(), // for shareable URLs + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); + +// ── Processing Jobs ───────────────────────────────── + +export const processingJobs = pgTable('processing_jobs', { + id: cuid2('id').primaryKey(), + projectId: cuid2('project_id').references(() => projects.id).notNull(), + stage: processingStageEnum('stage').default('feature_extraction').notNull(), + progress: integer('progress').default(0), // 0-100 per stage + error: text('error'), // error message if failed + startedAt: timestamp('started_at'), + completedAt: timestamp('completed_at'), + createdAt: timestamp('created_at').defaultNow().notNull(), +}); + +// ── Subscriptions ─────────────────────────────────── + +export const subscriptions = pgTable('subscriptions', { + id: cuid2('id').primaryKey(), + teamId: cuid2('team_id').references(() => teams.id).notNull(), + plan: subscriptionPlanEnum('plan').default('free').notNull(), + stripeCustomerId: text('stripe_customer_id'), + stripePriceId: text('stripe_price_id'), + stripeSubscriptionId: text('stripe_subscription_id'), + monthlyQuota: integer('monthly_quota').default(3), // Free=3, Pro=25, Enterprise=unlimited + monthlyUsage: integer('monthly_usage').default(0), + currentPeriodStart: timestamp('current_period_start'), + currentPeriodEnd: timestamp('current_period_end'), + createdAt: timestamp('created_at').defaultNow().notNull(), + updatedAt: timestamp('updated_at').defaultNow().notNull(), +}); + +// ── Sessions (better-auth managed) ────────────────── + +export const sessions = pgTable('sessions', { + id: cuid2('id').primaryKey(), + userId: cuid2('user_id').references(() => users.id).notNull(), + token: text('token').notNull().unique(), + expiresAt: timestamp('expires_at').notNull(), + ipAddress: text('ip_address'), + userAgent: text('user_agent'), + createdAt: timestamp('created_at').defaultNow().notNull(), +}); +``` + +## Indexes + +```sql +CREATE INDEX idx_projects_team_id ON projects(team_id); +CREATE INDEX idx_projects_status ON projects(status); +CREATE INDEX idx_photos_project_id ON photos(project_id); +CREATE INDEX idx_photos_perceptual_hash ON photos(perceptual_hash); +CREATE INDEX idx_walkthroughs_project_id ON walkthroughs(project_id); +CREATE INDEX idx_walkthroughs_slug ON walkthroughs(slug); +CREATE INDEX idx_processing_jobs_project_id ON processing_jobs(project_id); +CREATE INDEX idx_team_members_user_id ON team_members(user_id); +CREATE INDEX idx_team_members_team_id ON team_members(team_id); +CREATE INDEX idx_subscriptions_team_id ON subscriptions(team_id); +``` + +## Constraints + +| Rule | Level | Implementation | +|------|-------|---------------| +| User email unique | DB | `users.email UNIQUE` | +| Team slug unique | DB | `teams.slug UNIQUE` | +| Walkthrough slug unique | DB | `walkthroughs.slug UNIQUE` | +| Project belongs to team | DB | `projects.team_id FK → teams.id` | +| Photo belongs to project | DB | `photos.project_id FK → projects.id CASCADE` | +| Max 500 photos per project | App | Validate in Zod schema + upload endpoint | +| Monthly quota enforcement | App | Middleware checks `subscriptions.monthlyUsage < monthlyQuota` | +| Photo deduplication | App | Perceptual hash comparison before insert | \ No newline at end of file diff --git a/.specify/specs/001-interiorscan-mvp/plan.md b/.specify/specs/001-interiorscan-mvp/plan.md new file mode 100644 index 0000000..8316c43 --- /dev/null +++ b/.specify/specs/001-interiorscan-mvp/plan.md @@ -0,0 +1,131 @@ +# InteriorScan MVP — Implementation Plan + +> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task. + +**Goal:** Build an interior photogrammetry SaaS that turns apartment photos into navigable 3D walkthroughs (Doom-map style). + +**Architecture:** Turborepo monorepo (tpl-next-saas template). Next.js 16 App Router for web UI + dashboard. GPU worker (Node.js) orchestrates COLMAP/OpenMVS processing pipeline. three.js FPS viewer for walkthrough navigation. PostgreSQL + Drizzle for data. Valkey for queues/sessions. Stripe for billing. + +**Tech Stack:** Next.js 16 · Drizzle ORM · PostgreSQL · Valkey · three.js · COLMAP · OpenMVS · Stripe · Biome · Vitest + +--- + +## Constitution Check + +| Principle | Status | Notes | +|---|---|---| +| I. Self-Hosted First | ✅ PASS | All infra self-hosted except Stripe | +| II. Progressive Experience | ✅ PASS | WebGL → 360° fallback, mobile tap-to-walk | +| III. FPS-Grade Immersion | ✅ PASS | WASD + mouse primary, tap-to-walk secondary | +| IV. Photo-First Capture | ✅ PASS | Mobile photos as baseline, LiDAR enhancement | +| V. API-First Architecture | ✅ PASS | API endpoints defined before UI | +| VI. TypeScript Strict | ✅ PASS | strict + noUncheckedIndexedAccess | +| VII. Test-First | ⚠ CONDITIONAL | TDD for business logic; 3D viewer tested via Playwright E2E | +| VIII. Open Formats | ✅ PASS | OBJ export in MVP, E57/DXF/IFC in P4+ | + +⚠ VII conditional: 3D rendering correctness is validated via E2E visual regression, not unit tests. Business logic (upload, processing, auth) follows strict TDD. + +--- + +## Data Model + +See `data-model.md` (separate document — same directory). + +--- + +## Phase 1: Monorepo Scaffold + Core Infra (Shared) + +- [ ] T001 Create monorepo from tpl-next-saas template +- [ ] T002 Configure Drizzle schema with InteriorScan tables (projects, photos, walkthroughs, processing_jobs, users, teams, subscriptions) +- [ ] T003 Run migration: `pnpm db:migrate` +- [ ] T004 [P] Add seed data: 1 user, 1 team, 3 project statuses +- [ ] T005 [P] Set up Valkey connection for job queue +- [ ] T006 [P] Configure Docker Compose (postgres + valkey + web) + +**Checkpoint**: Migration green, `pnpm dev` starts without errors, health endpoint returns 200. + +--- + +## Phase 2: Photo Upload & Processing Pipeline (P1) 🎯 MVP Core + +### 2A: Upload API + +- [ ] T007 Write failing test: POST /api/v1/projects with photos returns 201 + project ID +- [ ] T008 Implement project creation endpoint with Zod validation +- [ ] T009 Write failing test: Upload 3 JPEGs → stored in object storage, DB has photo records +- [ ] T010 Implement photo upload with deduplication (perceptual hash) +- [ ] T011 Write failing test: GET /api/v1/projects/:id returns status + progress +- [ ] T012 Implement project status endpoint + +**Checkpoint**: Upload 40 photos via API → receive project ID → poll status → see "queued". + +### 2B: Processing Worker + +- [ ] T013 Set up `apps/processor/` — Node.js worker with BullMQ/Valkey job queue +- [ ] T014 Write failing test: Job queued → worker picks up → runs COLMAP SfM → produces sparse point cloud +- [ ] T015 Implement COLMAP SfM step (feature extraction + matching + sparse reconstruction) +- [ ] T016 Write failing test: SfM output → OpenMVS dense reconstruction → mesh file produced +- [ ] T017 Implement OpenMVS dense reconstruction step (MVS + meshing + texturing) +- [ ] T018 Write failing test: Mesh + textures → tiled output for three.js streaming +- [ ] T019 Implement mesh processing + tiling step (LOD generation, Draco compression, texture atlas) +- [ ] T020 Implement progress reporting (Valkey pub/sub → web UI) +- [ ] T021 Write failing test: Processing failure → job marked failed → user notified +- [ ] T022 Implement error handling + retry logic + +**Checkpoint**: Upload 40 photos → processing completes → produces OBJ mesh + tiled three.js output. Total time <60 min for 100 photos on GPU. + +--- + +## Phase 3: 3D Viewer — FPS Walkthrough (P1) 🎯 MVP Core + +- [ ] T023 Create `packages/viewer/` — three.js + Vite library package +- [ ] T024 Implement FPS camera controller (WASD + mouse look, collision detection, gravity) +- [ ] T025 Implement tap-to-walk controller (mobile: tap destination → navmesh pathfinding) +- [ ] T026 Implement mesh loader (tiled LODeD mesh streaming, Draco decompression) +- [ ] T027 Implement texture loading (progressive: low-res first, high-res on demand) +- [ ] T028 Implement mini-map overlay (bird's eye view with player position) +- [ ] T029 Implement measurement overlay toggle (room dimensions on walls/floor) +- [ ] T030 Implement WebGL fallback detection → 360° photo carousel mode +- [ ] T031 Implement touch controls (gyroscope look, pinch-zoom, two-finger walk) +- [ ] T032 Write Playwright E2E test: load walkthrough → verify 3D renders → verify WASD movement → verify tap-to-walk + +**Checkpoint**: Open walkthrough link → walk through reconstructed space at 30+ FPS. WASD works. Click-to-walk works. Mini-map shows position. + +--- + +## Phase 4: Web UI — Dashboard & Sharing (P1 + P2) + +- [ ] T033 Create project creation flow (multi-step: name/address → photo upload → processing) +- [ ] T034 Create processing status page (progress bar, ETA, stage indicator) +- [ ] T035 Create walkthrough view page (embedded viewer + share controls) +- [ ] T036 Implement share link generation (public, no auth required) +- [ ] T037 Implement oEmbed embed code generator +- [ ] T038 Create dashboard grid (project cards with thumbnail, status, date) +- [ ] T039 Create project settings (edit name, address, delete) + +**Checkpoint**: Agent creates project → uploads photos → sees processing progress → walkthrough appears → shares link → visitor navigates without login. + +--- + +## Phase 5: Auth, Teams & Billing (P3) + +- [ ] T040 Set up better-auth with email + Google OAuth +- [ ] T041 Implement RBAC (owner/admin/member/viewer per team) +- [ ] T042 Create subscription tiers (Free: 3/month, Pro: 25/month, Enterprise: unlimited) +- [ ] T043 Implement Stripe checkout + webhook +- [ ] T044 Implement usage tracking + quota enforcement middleware + +**Checkpoint**: User signs up → creates 3 projects on Free → gets upgrade prompt → pays via Stripe → quota increases. + +--- + +## Phase 6: Polish & Launch + +- [ ] T045 Landing page with demo walkthrough embedded +- [ ] T046 Pricing page with 3 tiers +- [ ] T047 Email notifications (processing complete, processing failed, quota warning) +- [ ] T048 Mobile-responsive dashboard with bottom nav +- [ ] T049 Accessibility audit (WCAG 2.2 AA): keyboard nav, screen reader, contrast +- [ ] T050 Performance audit (Core Web Vitals, first mesh load <5s on 4G) + +**Checkpoint**: Real tester (monyi's friends) create walkthrough from phone photos in <1h total. Lighthouse 90+. \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..685e6f7 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,71 @@ +# InteriorScan — AGENTS.md + +## Project Overview + +InteriorScan is a SaaS platform for interior photogrammetry targeting real estate. Users upload photos of a property, the system reconstructs a 3D mesh, and visitors navigate the space in first-person (WASD + click-to-walk) — like a Doom map of a real apartment. + +**Purpose**: Filter properties before physical visits. Save time for buyers and agents. + +**Stack**: Next.js 16 App Router · Turborepo · Drizzle ORM · PostgreSQL · Valkey · three.js · COLMAP/OpenMVS · Stripe + +**Repo**: https://git.d0a1.es/hermes/interiorscan + +## Architecture + +``` +interiorscan/ +├── apps/web/ # Next.js 16 App Router (SSR + client) +│ ├── src/app/(marketing)/ # Landing, pricing, public pages +│ ├── src/app/(saas)/ # Auth-guarded dashboard +│ ├── src/app/api/ # Webhooks, health, processing callbacks +│ └── src/lib/3d/ # three.js viewer components (FPS controls, mesh loader) +├── apps/processor/ # Node.js worker — orchestrates COLMAP/OpenMVS pipeline +├── packages/ +│ ├── auth/ # better-auth + RBAC +│ ├── db/ # Drizzle schema + migrations +│ ├── ui/ # shadcn/ui v4 + Tailwind tokens +│ ├── utils/ # Zod schemas, neverthrow, error catalog +│ └── ts-config/ # Shared TS configs +├── docker/ # Docker Compose (web + postgres + valkey + GPU worker) +└── .specify/ # Constitution, specs, plans +``` + +## Constitution + +See `.specify/memory/constitution.md` — all development must comply. + +## Key Decisions + +- **FPS navigation**: WASD + click-to-walk (both modes). Not 360° panos. +- **Mobile-first capture**: iPhone/Android photos as baseline. LiDAR = enhancement. +- **Self-hosted**: All infra self-hosted except Stripe. +- **API-first**: Every feature has REST API before UI. +- **Open exports**: OBJ, E57, LAS/LAZ, DXF minimum. IFC in P4+. + +## Development Commands + +```bash +pnpm install # Install dependencies +pnpm dev # Start dev server (all apps) +pnpm build # Production build +pnpm test # Run Vitest +pnpm lint # Biome check +pnpm typecheck # TypeScript strict check +``` + +## Standards Reference + +| Layer | Choice | Standard | +|---|---|---| +| Auth | better-auth | [SECURITY.md](https://git.d0a1.es/d0a1-agentic-crew/agentic-standards/src/branch/main/standards/SECURITY.md) | +| Design | d0a1 palettes | [DESIGN_SYSTEM.md](https://git.d0a1.es/d0a1-agentic-crew/agentic-standards/src/branch/main/standards/DESIGN_SYSTEM.md) | +| CSS | #app[data-theme] selectors | Design system token pattern | +| Lint | Biome 2.x only | [STANDARDS.md](https://git.d0a1.es/d0a1-agentic-crew/agentic-standards/src/branch/main/standards/STANDARDS.md) | +| Testing | Vitest v4 + Playwright | tpl-next-saas pattern | + +## Branch Strategy + +- `main` — production-ready +- `feat/###-description` — feature branches from spec IDs +- `fix/description` — bugfixes +- Conventional commits: `feat:`, `fix:`, `docs:`, `chore:`, `refactor:` \ No newline at end of file diff --git a/README.md b/README.md index 57e873d..79c712b 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,59 @@ -# interiorscan +# InteriorScan -Interior photogrammetry SaaS - Doom-map style 3D walkthroughs for real estate \ No newline at end of file +**Turn apartment photos into navigable 3D walkthroughs.** + +InteriorScan is a self-hosted SaaS platform for interior photogrammetry targeting real estate. Upload 30-100 photos of a property, the system reconstructs a 3D mesh (COLMAP + OpenMVS), and visitors navigate the space in first-person — like a Doom map of a real home. + +## Why? + +Physical property visits are time-consuming and often unnecessary. InteriorScan lets potential buyers walk through a reconstructed interior from their browser, filtering out properties that don't fit before scheduling a visit. + +## Features + +- 📸 **Photo-first capture** — iPhone/Android photos as baseline, LiDAR optional +- 🎮 **FPS navigation** — WASD + mouse (desktop), tap-to-walk (mobile) +- 🔗 **Share & embed** — public links + oEmbed for listing pages +- 📐 **Measurements** — room dimensions overlaid on walkthrough +- 🗺️ **Floor plans** — DXF/PDF export from reconstructed geometry +- 🔌 **API-first** — every feature has REST API before UI +- 🏠 **Self-hosted** — PostgreSQL + Valkey + GPU worker, no vendor lock-in + +## Stack + +| Layer | Technology | +|-------|-----------| +| Framework | Next.js 16 (App Router) | +| Monorepo | Turborepo + pnpm | +| Database | PostgreSQL + Drizzle ORM | +| Cache/Queue | Valkey | +| 3D Viewer | three.js | +| Processing | COLMAP + OpenMVS | +| Auth | better-auth | +| Billing | Stripe | +| Lint | Biome 2.x | +| Testing | Vitest v4 + Playwright | + +## Quick Start + +```bash +pnpm install +pnpm dev +``` + +## Project Structure + +``` +interiorscan/ +├── apps/web/ # Next.js 16 App Router +├── apps/processor/ # GPU processing worker +├── packages/auth/ # better-auth + RBAC +├── packages/db/ # Drizzle schema + migrations +├── packages/viewer/ # three.js FPS walkthrough viewer +├── packages/ui/ # shadcn/ui v4 + Tailwind tokens +├── packages/utils/ # Zod, neverthrow, error catalog +└── .specify/ # Constitution, specs, plans +``` + +## License + +Apache-2.0 \ No newline at end of file