Skip to content

Commit 1e97ec6

Browse files
committed
Build Milestone 1 foundation
1 parent 32de3b5 commit 1e97ec6

111 files changed

Lines changed: 12580 additions & 26 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.dockerignore

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
.git
2+
.github
3+
.env
4+
.env.*
5+
!.env.example
6+
.venv
7+
venv
8+
__pycache__
9+
*.py[cod]
10+
.pytest_cache
11+
.mypy_cache
12+
.ruff_cache
13+
node_modules
14+
.next
15+
coverage
16+
dist
17+
out
18+
data/raw
19+
data/processed
20+
*.gpkg
21+
*.geojson
22+
*.shp
23+
*.shx
24+
*.dbf
25+
*.parquet
26+
*.tif
27+
*.tiff
28+
.DS_Store
29+
.idea
30+
.vscode
31+
*.log
32+
.railway

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
# Runtime
22
APP_ENV=development
3+
APP_VERSION=0.1.0
34
LOG_LEVEL=INFO
5+
DATASET_MODE=synthetic
6+
INGESTION_ENABLED=false
7+
ALERT_DELIVERY_MODE=log
48
PUBLIC_APP_URL=http://localhost:3000
59
API_BASE_URL=http://localhost:8000
10+
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
611
ALLOWED_ORIGINS=http://localhost:3000
712

813
# Persistence

.github/workflows/ci.yml

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [main]
7+
8+
permissions:
9+
contents: read
10+
11+
concurrency:
12+
group: ci-${{ github.workflow }}-${{ github.ref }}
13+
cancel-in-progress: true
14+
15+
jobs:
16+
configuration:
17+
name: Configuration and secrets
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v4
21+
with:
22+
fetch-depth: 0
23+
- uses: actions/setup-python@v5
24+
with:
25+
python-version: "3.13"
26+
- run: python -m pip install --disable-pip-version-check PyYAML==6.0.2
27+
- run: python scripts/ci/validate_config.py
28+
- run: docker compose config --quiet
29+
- run: scripts/ci/check-secrets.sh
30+
- uses: gitleaks/gitleaks-action@v2
31+
env:
32+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
33+
34+
python:
35+
name: Python quality gates
36+
runs-on: ubuntu-latest
37+
services:
38+
postgis:
39+
image: postgis/postgis:17-3.5
40+
env:
41+
POSTGRES_DB: seekandscore_test
42+
POSTGRES_USER: seekandscore
43+
POSTGRES_PASSWORD: seekandscore
44+
ports:
45+
- 5432:5432
46+
options: >-
47+
--health-cmd "pg_isready -U seekandscore -d seekandscore_test"
48+
--health-interval 5s
49+
--health-timeout 5s
50+
--health-retries 20
51+
redis:
52+
image: redis:7.4-alpine
53+
ports:
54+
- 6379:6379
55+
options: >-
56+
--health-cmd "redis-cli ping"
57+
--health-interval 5s
58+
--health-timeout 3s
59+
--health-retries 20
60+
env:
61+
APP_ENV: test
62+
DATABASE_URL: postgresql+psycopg://seekandscore:seekandscore@localhost:5432/seekandscore_test
63+
REDIS_URL: redis://localhost:6379/15
64+
DATASET_MODE: synthetic
65+
INGESTION_ENABLED: "false"
66+
OUTREACH_MODE: disabled
67+
OUTREACH_SEND_ENABLED: "false"
68+
steps:
69+
- uses: actions/checkout@v4
70+
- uses: astral-sh/setup-uv@v6
71+
with:
72+
version: "0.8.13"
73+
enable-cache: true
74+
- run: uv sync --frozen --group dev
75+
- run: uv run ruff check python services tests/backend migrations
76+
- run: uv run ruff format --check python services tests/backend migrations
77+
- run: uv run mypy python services
78+
- run: uv run pytest tests/backend
79+
- run: uv run python -m seekandscore.db.migrate upgrade
80+
81+
web:
82+
name: Web quality gates
83+
runs-on: ubuntu-latest
84+
steps:
85+
- uses: actions/checkout@v4
86+
- uses: pnpm/action-setup@v4
87+
with:
88+
version: 10.2.1
89+
- uses: actions/setup-node@v4
90+
with:
91+
node-version: "22.14.0"
92+
cache: pnpm
93+
- run: pnpm install --frozen-lockfile
94+
- run: pnpm check
95+
96+
containers:
97+
name: Container build (${{ matrix.name }})
98+
runs-on: ubuntu-latest
99+
needs: [configuration, python, web]
100+
strategy:
101+
fail-fast: false
102+
matrix:
103+
include:
104+
- name: api
105+
dockerfile: infra/docker/api.Dockerfile
106+
- name: worker
107+
dockerfile: infra/docker/worker.Dockerfile
108+
- name: web
109+
dockerfile: infra/docker/web.Dockerfile
110+
steps:
111+
- uses: actions/checkout@v4
112+
- uses: docker/setup-buildx-action@v3
113+
- uses: docker/build-push-action@v6
114+
with:
115+
context: .
116+
file: ${{ matrix.dockerfile }}
117+
push: false
118+
tags: seekandscore/${{ matrix.name }}:ci
119+
cache-from: type=gha,scope=${{ matrix.name }}
120+
cache-to: type=gha,mode=max,scope=${{ matrix.name }}

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ __pycache__/
2121
.pytest_cache/
2222
.mypy_cache/
2323
.ruff_cache/
24+
.coverage
25+
htmlcov/
2426

2527
# Data and geospatial artifacts
2628
data/raw/
@@ -40,3 +42,9 @@ data/processed/
4042
.vscode/
4143
*.log
4244
.railway/
45+
46+
# Local service data and overrides
47+
compose.override.yaml
48+
.minio/
49+
output/playwright/
50+
.playwright-cli/

Makefile

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
.DEFAULT_GOAL := help
2+
3+
.PHONY: help install check lint test build infra-up infra-down infra-logs infra-status app-up compose-config config-check secret-check
4+
5+
help: ## List common development commands.
6+
@awk 'BEGIN {FS = ":.*## "} /^[a-zA-Z0-9_-]+:.*## / {printf "%-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
7+
8+
install: ## Install locked Python and JavaScript dependencies.
9+
uv sync --frozen --group dev
10+
corepack enable
11+
pnpm install --frozen-lockfile
12+
13+
check: config-check secret-check ## Run repository quality gates.
14+
uv run ruff check python services tests/backend migrations
15+
uv run ruff format --check python services tests/backend migrations
16+
uv run mypy python services
17+
uv run pytest tests/backend
18+
pnpm check
19+
20+
lint: ## Run Python and web linters.
21+
uv run ruff check python services tests/backend migrations
22+
pnpm lint
23+
24+
test: ## Run Python and web tests.
25+
uv run pytest tests/backend
26+
pnpm test
27+
28+
build: ## Build the production web bundle.
29+
pnpm build
30+
31+
infra-up: ## Start local PostGIS, Redis, and MinIO.
32+
docker compose up --detach --wait postgis redis minio
33+
docker compose run --rm minio-init
34+
35+
infra-down: ## Stop local infrastructure without deleting data volumes.
36+
docker compose down
37+
38+
infra-logs: ## Follow local infrastructure logs.
39+
docker compose logs --follow postgis redis minio
40+
41+
infra-status: ## Show local service status.
42+
docker compose ps
43+
44+
app-up: ## Build and start the full local stack with safe defaults.
45+
docker compose --profile app up --build
46+
47+
compose-config: ## Validate and render the Compose model.
48+
docker compose config --quiet
49+
50+
config-check: ## Parse YAML and TOML configuration files.
51+
uv run python scripts/ci/validate_config.py
52+
53+
secret-check: ## Scan tracked text for high-confidence credential patterns.
54+
scripts/ci/check-secrets.sh

README.md

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Seek and Score is a planning-first, explainable property-intelligence platform for discovering, underwriting, and ranking real-estate investment opportunities. Central Texas is the launch market; the architecture is intentionally designed to add county-by-county data adapters and Opportunity Zone cohorts across the United States without forking the core product.
44

5-
> Status: architecture and delivery planning. No production application or live investment recommendations exist yet.
5+
> Status: Milestone 1 foundation. A synthetic-only operator console, API, workers, database migrations, local infrastructure, CI, and Railway deployment templates are runnable. Live data ingestion, automated outreach, and production investment recommendations remain disabled.
66
77
## North-star outcome
88

@@ -59,6 +59,41 @@ Parcel location is also not a legal conclusion that an investment qualifies for
5959

6060
See [Opportunity Zone and national data design](docs/OPPORTUNITY_ZONES_AND_DATA.md).
6161

62+
## What runs today
63+
64+
The foundation slice is intentionally useful without implying that the product is ready for live acquisitions:
65+
66+
- a responsive Next.js operator console with a synthetic Top 25 queue, evidence, risks, filters, and candidate drill-down;
67+
- a FastAPI read API with health, version, capabilities, synthetic candidate list, and candidate-detail endpoints;
68+
- modular backend boundaries for platform, registry, identity, and engagement;
69+
- policy-gated outreach states where every outbound channel and send action fails closed;
70+
- Alembic foundations for the registry, identity, engagement, and platform schemas;
71+
- worker and scheduler entrypoints with inert defaults;
72+
- PostGIS, Redis, and private MinIO services for local development;
73+
- non-root production containers, continuous integration, configuration validation, and credential scanning.
74+
75+
The web application prefers the API when `API_BASE_URL` is set and healthy. It falls back to an explicitly labeled synthetic fixture when the API is unavailable, so a preview never silently turns into a live-data product.
76+
77+
## Quick start
78+
79+
Prerequisites are Python 3.12+, [uv](https://docs.astral.sh/uv/), Node.js 22+, pnpm 10+, and Docker for the infrastructure profile.
80+
81+
```bash
82+
make install
83+
make check
84+
```
85+
86+
Run the API and web application in separate terminals:
87+
88+
```bash
89+
uv run python -m seekandscore.api
90+
pnpm dev
91+
```
92+
93+
Then open `http://localhost:3000`. The API exposes `http://localhost:8000/readyz`, `/version`, `/v1/capabilities`, and `/v1/candidates`.
94+
95+
For local persistence services, use `make infra-up`. To build and run the complete container stack with the same safe defaults, use `make app-up`.
96+
6297
## Repository guide
6398

6499
- [Implementation plan](docs/IMPLEMENTATION_PLAN.md) — phases, deliverables, acceptance criteria, dependencies, and risks
@@ -85,7 +120,7 @@ See [Opportunity Zone and national data design](docs/OPPORTUNITY_ZONES_AND_DATA.
85120
10. Acquisition outreach is party-verified, policy-gated, human-approved, suppressible, and auditable.
86121
11. A second, dissimilar market must prove the abstraction before national expansion.
87122

88-
## Proposed monorepo shape
123+
## Monorepo shape
89124

90125
```text
91126
apps/
@@ -97,25 +132,26 @@ packages/
97132
contracts/ OpenAPI, JSON Schema, generated clients
98133
ui/ shared TypeScript UI primitives
99134
python/seekandscore/
100-
modules/ backend bounded contexts
101-
adapters/ source-provider implementations
102-
config/
103-
regions/ geography and jurisdiction packs
104-
scoring/ versioned deterministic score models
105-
outreach/ versioned channel and jurisdiction-safe defaults
135+
platform/ configuration and shared application primitives
136+
registry/ geography-neutral property registry contracts
137+
identity/ parties, roles, and identity evidence
138+
engagement/ policy-gated contact and scheduling boundary
106139
infra/
140+
docker/ production container definitions
107141
railway/ service configuration and runbooks
108142
docs/
109143
adr/ architecture decision records
144+
tests/
145+
backend/ API, settings, read-model, and runtime tests
110146
```
111147

112-
This structure is the target scaffold for Milestone 1; the current repository intentionally begins with decisions and execution criteria before code.
148+
Source adapters, regional packs, scoring configurations, and live provider integrations will be added behind these boundaries in later milestones.
113149

114150
## Railway strategy
115151

116-
Railway will host stateless web/API/worker processes and the initial Redis/PostGIS services. The MVP can use a single Railway PostGIS node with tested backups. Railway's native PostgreSQL high-availability conversion does not support the community PostGIS image, so production scale has an explicit decision gate: accept the documented single-node risk or move PostGIS to a managed HA provider while leaving the applications on Railway.
152+
Railway hosts the stateless web/API/worker processes and can host the initial Redis/PostGIS services. The MVP can use a single Railway PostGIS node with tested backups. Railway's native PostgreSQL high-availability conversion does not support the community PostGIS image, so production scale has an explicit decision gate: accept the documented single-node risk or move PostGIS to a managed HA provider while leaving the applications on Railway.
117153

118-
No Railway project is created in this planning commit. Deployment starts after the foundation service has health endpoints, migrations, a synthetic fixture dataset, and a restore-tested database.
154+
The initial staging release deploys only synthetic application behavior with ingestion and outreach disabled. Database-backed ingestion is activated only after the PostGIS backup/restore path and source rights are verified; provider outreach remains a separate production activation decision.
119155

120156
## Important boundaries
121157

apps/web/app/api/health/route.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { NextResponse } from "next/server";
2+
3+
export const dynamic = "force-dynamic";
4+
5+
export function GET() {
6+
return NextResponse.json(
7+
{
8+
status: "ok",
9+
service: "seekandscore-web",
10+
version: process.env.RAILWAY_GIT_COMMIT_SHA?.slice(0, 12) ?? "development",
11+
timestamp: new Date().toISOString(),
12+
},
13+
{
14+
headers: {
15+
"Cache-Control": "no-store",
16+
},
17+
},
18+
);
19+
}

apps/web/app/error.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
5+
import { Button } from "@seekandscore/ui";
6+
7+
export default function ErrorPage({
8+
error,
9+
reset,
10+
}: {
11+
error: Error & { digest?: string };
12+
reset: () => void;
13+
}) {
14+
useEffect(() => {
15+
console.error("Operator console render failed", error);
16+
}, [error]);
17+
18+
return (
19+
<main className="state-page">
20+
<section className="state-card">
21+
<span className="state-card__code">VIEW UNAVAILABLE</span>
22+
<h1>The investment queue could not load</h1>
23+
<p>
24+
The current snapshot is unchanged. Retry the view; no decisions or outreach actions
25+
were submitted.
26+
</p>
27+
<Button onClick={reset} variant="primary">
28+
Retry view
29+
</Button>
30+
</section>
31+
</main>
32+
);
33+
}

0 commit comments

Comments
 (0)