Skip to content

Commit 1921ebf

Browse files
docs(sdd): archive scaffold-prod change and sync spec
1 parent d42e04f commit 1921ebf

7 files changed

Lines changed: 224 additions & 0 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Archive Report — scaffold-prod (P2 production-ready scaffolding)
2+
3+
**Status**: ARCHIVED
4+
**Change**: scaffold-prod
5+
**Merged**: PR #52#55 (slices) + PR #56 (tracker) → main (commit d42e04f)
6+
**Date**: 2026-08-14
7+
8+
## Executive Summary
9+
10+
Closed the P2 scaffolding features found in real-world use (portfolio-go). Every new scaffolded project is production-ready: stdlib typed config, subcommand-aware main, zero-dep SQL migrations runner, plus compose/Dockerfile/env driver-consistency bug fixes. Determinism fix (removed `generated_at: {{ now }}` → static `scaffold_prod_v1` marker). Verify: 9 packages green, scaffold matrix + upgrade injection tests, live smoke via MCP new_project.
11+
12+
## What Ships
13+
14+
- **Typed config** (`internal/config`): stdlib `Load()` with defaults (8080/development), fail-fast on missing `DATABASE_URL` for DB projects (points to .env.example).
15+
- **Subcommand main**: `switch os.Args[1]` — default = `server` (preserves `CMD ["./main"]`), `migrate`, `version`, unknown → exit 2. Conditional on the marker: `{{ if .ScaffoldProdV1 }}` dispatch + config `{{ else }}` legacy `{{ end }}` — no-marker projects keep byte-identical legacy mains.
16+
- **Migrations runner** (`internal/dbmigrate`): `//go:embed migrations/*.sql`, `Up(driverName, dsn)` idempotent via `schema_migrations` (VARCHAR(255) PK — MySQL-safe), tx-wrapped exec+record, per-driver blank import (pgx stdlib / mysql), `?` placeholder. Generated for PostgreSQL|MySQL only; MongoDB gets env note; None gets nothing.
17+
- **Driver pins**: pgx v5.7.1 (PostgreSQL), go-sql-driver/mysql v1.8.1 (MySQL) — new scaffolds only (go.mod report-only on upgrade).
18+
- **Upgrade injection**: marker'd projects that lose config/dbmigrate get them re-injected (routes.go precedent + `FileAction.TemplatePath` so injected files re-render from their own template; migrations SQL injected with the runner — empty go:embed is a compile error).
19+
- **Compose/Dockerfile/env fixes**: DATABASE_URL per driver, volume per driver (pg/mysql/mongo paths), Minimalist Dockerfile build path (`./main.go` vs `./cmd/api/main.go`), `POSTGRES_*` only for PostgreSQL.
20+
- **Determinism**: `generated_at: {{ now }}` removed (broke upgrade byte-stable re-render) → replaced by static `scaffold_prod_v1: true`.
21+
22+
## Verification Summary
23+
24+
- Full suite green (`go test ./...`, 9 packages), vet + gofmt clean.
25+
- Scaffold matrix: TestScaffoldProd_DispatchInMains (3 arches × dispatch/subcommands/exit-2/config import), TestScaffoldProd_DBMigrateMatrix (4 drivers × runner presence + VARCHAR PK + real SQL statement), TestUpgrade_InjectsScaffoldProd (marker injects 3 files + Apply writes; no-marker nothing), TestEngine_DeterministicConfig (no generated_at, byte-identical re-render).
26+
- Smoke (MCP new_project, PostgreSQL+web): config.go + dbmigrate/migrate.go + migrations/0001_init.sql + dispatch main + marker + pgx pin all generated; runMigrate calls `dbmigrate.Up("pgx", cfg.DatabaseURL)`.
27+
- SDD: explore → propose (8 decisions) → spec (6 req / 23 scenarios) → design (fresh-context validator: 4 blockers + 8 findings — hexagonal main missing, marker contract self-contradiction, MySQL DDL TEXT PK, injection path incomplete) → tasks (23) → apply (4 slices).
28+
29+
## Follow-Ups (non-blocking)
30+
31+
- MongoDB migrations runner — relational-only by design.
32+
- `create-admin` / app-specific subcommands — out of scaffold scope (user adds them per project).
33+
- Down migrations — Up-only.
34+
- The `?` placeholder works for both pgx stdlib and MySQL — validated by inspection; a MySQL live smoke is recommended before the next release (PostgreSQL smoke only this cycle).
35+
36+
## Artifacts
37+
38+
| Artifact | Path |
39+
|----------|------|
40+
| Proposal | `openspec/changes/archive/2026-08-14-scaffold-prod/proposal.md` |
41+
| Exploration | `openspec/changes/archive/2026-08-14-scaffold-prod/exploration.md` |
42+
| Design | `openspec/changes/archive/2026-08-14-scaffold-prod/design.md` |
43+
| Tasks | `openspec/changes/archive/2026-08-14-scaffold-prod/tasks.md` |
44+
| Spec (delta) | `openspec/changes/archive/2026-08-14-scaffold-prod/specs/scaffold-prod/spec.md` |
45+
| Spec (synced) | `openspec/specs/scaffold-prod/spec.md` (6 requirements, byte-identical) |
46+
47+
## Delivery Note
48+
49+
Receipt-driven review disabled at clone scope (user decision after escalating upstream #2743). Delivery under ordinary policy — CI gates (test/lint) are the authority. No review receipt exists; none fabricated.
File renamed without changes.

openspec/changes/scaffold-prod/exploration.md renamed to openspec/changes/archive/2026-08-14-scaffold-prod/exploration.md

File renamed without changes.
File renamed without changes.

openspec/changes/scaffold-prod/specs/scaffold-prod/spec.md renamed to openspec/changes/archive/2026-08-14-scaffold-prod/specs/scaffold-prod/spec.md

File renamed without changes.
File renamed without changes.
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Delta for scaffold-prod
2+
3+
## ADDED Requirements
4+
5+
### Requirement: Typed Config Package (typed-config)
6+
7+
Every scaffolded project MUST generate an `internal/config` package with a `Config` struct containing `ServerPort string`, `AppEnv string`, and `DatabaseURL string`. The package MUST provide a stdlib-only `Load() (*Config, error)` that reads `SERVER_PORT` (default `"8080"`), `APP_ENV` (default `"development"`), and `DATABASE_URL` from the environment. When the project was generated with a database driver other than `None`, `Load()` MUST fail fast with an error naming the missing variable and pointing to `.env.example` if `DATABASE_URL` is unset. All generated code MUST be deterministic (no timestamps, no randomness) so upgrade re-render is byte-stable.
8+
9+
#### Scenario: Default config loads
10+
11+
- GIVEN a generated project with no environment variables set
12+
- WHEN `config.Load()` runs
13+
- THEN it returns `Config{ServerPort: "8080", AppEnv: "development", DatabaseURL: ""}` with no error
14+
15+
#### Scenario: Env vars override defaults
16+
17+
- GIVEN `SERVER_PORT=9000` and `APP_ENV=production` in the environment
18+
- WHEN `config.Load()` runs
19+
- THEN it returns `ServerPort: "9000"` and `AppEnv: "production"`
20+
21+
#### Scenario: Missing DATABASE_URL fails fast for DB projects
22+
23+
- GIVEN a project generated with `db_driver: PostgreSQL`
24+
- AND `DATABASE_URL` is not set
25+
- WHEN `config.Load()` runs
26+
- THEN it returns an error naming `DATABASE_URL`
27+
- AND the error message points to `.env.example`
28+
29+
#### Scenario: No database means no fail-fast
30+
31+
- GIVEN a project generated with `db_driver: None`
32+
- AND `DATABASE_URL` is not set
33+
- WHEN `config.Load()` runs
34+
- THEN it returns a config with empty `DatabaseURL` and no error
35+
36+
### Requirement: Subcommand Dispatch in Main (subcommand-dispatch)
37+
38+
Every generated `main.go` / `cmd/api/main.go` MUST dispatch on `os.Args[1]` with a stdlib `switch`. The default (no argument) MUST run the HTTP server — preserving the Dockerfile `CMD ["./main"]` contract. The `server` subcommand MUST run the same HTTP server logic explicitly. The `migrate` subcommand MUST run the migrations runner (only when the project has a database). The `version` subcommand MUST print a version string. Any unknown subcommand MUST print a usage hint and exit with code 2. The generated code MUST use only the standard library (no Cobra) so `go.mod` stays report-only-safe on upgrade.
39+
40+
#### Scenario: No argument runs the server
41+
42+
- GIVEN a generated project
43+
- WHEN it is executed as `./main`
44+
- THEN the HTTP server starts on the configured port
45+
46+
#### Scenario: Explicit server subcommand
47+
48+
- GIVEN a generated project
49+
- WHEN it is executed as `./main server`
50+
- THEN the HTTP server starts on the configured port
51+
52+
#### Scenario: Version subcommand
53+
54+
- GIVEN a generated project
55+
- WHEN it is executed as `./main version`
56+
- THEN it prints a version string and exits 0
57+
58+
#### Scenario: Unknown subcommand exits 2
59+
60+
- GIVEN a generated project
61+
- WHEN it is executed as `./main frobnicate`
62+
- THEN it prints a usage hint listing valid subcommands
63+
- AND it exits with code 2
64+
65+
#### Scenario: Migrate subcommand with database
66+
67+
- GIVEN a project generated with `db_driver: PostgreSQL`
68+
- WHEN it is executed as `./main migrate`
69+
- THEN the migrations runner executes pending migrations
70+
71+
### Requirement: Migrations Runner (migrations-runner)
72+
73+
When a project is generated with a database driver other than `None`, the scaffold MUST generate an `internal/dbmigrate` package that embeds migration SQL files via `//go:embed migrations/*.sql` (SQL files MUST live inside the runner package directory — go:embed cannot traverse above it). The package MUST provide `Up(driverName, dsn string) error` using `database/sql` and a `schema_migrations` tracking table so re-running is idempotent (already-applied migrations are skipped). The runner MUST support PostgreSQL (via `pgx/v5` stdlib driver) and MySQL (via `go-sql-driver/mysql`). MongoDB projects MUST NOT generate a runner; instead the README MUST note that migrations are not scaffolded for MongoDB.
74+
75+
#### Scenario: Migrations embedded and idempotent
76+
77+
- GIVEN a PostgreSQL project with `internal/dbmigrate/migrations/0001_init.sql`
78+
- WHEN `dbmigrate.Up("pgx", dsn)` runs twice
79+
- THEN both runs succeed
80+
- AND the second run skips the already-applied migration (schema_migrations records it)
81+
82+
#### Scenario: MySQL project has runner
83+
84+
- GIVEN a project generated with `db_driver: MySQL`
85+
- WHEN the scaffold runs
86+
- THEN `internal/dbmigrate` exists with the MySQL driver blank import
87+
88+
#### Scenario: MongoDB has no runner
89+
90+
- GIVEN a project generated with `db_driver: MongoDB`
91+
- WHEN the scaffold runs
92+
- THEN no `internal/dbmigrate` package is generated
93+
- AND the README notes migrations are not scaffolded for MongoDB
94+
95+
#### Scenario: No database has no runner
96+
97+
- GIVEN a project generated with `db_driver: None`
98+
- WHEN the scaffold runs
99+
- THEN no `internal/dbmigrate` package is generated
100+
101+
### Requirement: Upgrade Injection of New Packages (upgrade-injection)
102+
103+
The `upgrade` command MUST extend its routes.go absent→create precedent: during the post-classification loop, when a re-rendered main imports `internal/config` or `internal/dbmigrate` and the corresponding package directory is absent on disk, the CLI MUST render and write those scaffold-owned files with `origin: scaffold` so the upgraded project still compiles. This injection MUST be gated by a `.go-arch.yaml` marker `scaffold_prod_v1: true` written when the feature was scaffolded. Projects without the marker MUST NOT receive injected packages (they keep their legacy mains).
104+
105+
#### Scenario: Old project upgrades to subcommand main
106+
107+
- GIVEN a pre-feature project (no `scaffold_prod_v1` marker) with an old flat main
108+
- WHEN `go-arch upgrade` runs
109+
- THEN the main is re-rendered to the subcommand version
110+
- AND `internal/config` is written with `origin: scaffold` because it is absent
111+
- AND the project still compiles
112+
113+
#### Scenario: Marker gates injection
114+
115+
- GIVEN a project without the `scaffold_prod_v1` marker
116+
- WHEN `go-arch upgrade` runs
117+
- THEN no new packages are injected
118+
- AND the legacy main is preserved
119+
120+
#### Scenario: New scaffolds always have the marker
121+
122+
- GIVEN a newly scaffolded project
123+
- THEN `.go-arch.yaml` contains `scaffold_prod_v1: true`
124+
- AND `internal/config` exists from the start
125+
126+
### Requirement: Compose and Dockerfile Driver Fixes (compose-dockerfile-fixes)
127+
128+
The generated `docker-compose.yaml` MUST build the `DATABASE_URL` from the actual driver (not hardcoded PostgreSQL) and MUST mount the database volume at the driver-correct path (PostgreSQL `/var/lib/postgresql/data`, MySQL `/var/lib/mysql`). The generated `Dockerfile` MUST build the correct main path per architecture: `./main.go` for Minimalist, `./cmd/api/main.go` for Standard and Hexagonal. The generated `.env.example` MUST NOT leak `POSTGRES_USER`/`POSTGRES_PASSWORD` into projects without a PostgreSQL database.
129+
130+
#### Scenario: Minimalist Dockerfile builds main.go
131+
132+
- GIVEN a Minimalist project generated with `use_docker: true`
133+
- WHEN the Dockerfile is generated
134+
- THEN the build command is `go build -o main ./main.go`
135+
136+
#### Scenario: Standard Dockerfile builds cmd/api
137+
138+
- GIVEN a Standard project generated with `use_docker: true`
139+
- WHEN the Dockerfile is generated
140+
- THEN the build command is `go build -o main ./cmd/api/main.go`
141+
142+
#### Scenario: MySQL compose URL and volume
143+
144+
- GIVEN a project generated with `db_driver: MySQL`
145+
- WHEN docker-compose.yaml is generated
146+
- THEN `DATABASE_URL` uses the MySQL driver prefix
147+
- AND the volume mounts at `/var/lib/mysql`
148+
149+
#### Scenario: No PostgreSQL means no POSTGRES_* leak
150+
151+
- GIVEN a project generated with `db_driver: None` or `db_driver: MySQL`
152+
- WHEN `.env.example` is generated
153+
- THEN it does not contain `POSTGRES_USER` or `POSTGRES_PASSWORD`
154+
155+
### Requirement: Driver Dependencies (driver-pins)
156+
157+
New scaffolds generated with a database driver MUST pin the driver library in `go.mod`: `github.com/jackc/pgx/v5` for PostgreSQL and `github.com/go-sql-driver/mysql` for MySQL. This applies to NEW scaffolds only — `go.mod` remains report-only on upgrade, so existing projects are not modified.
158+
159+
#### Scenario: PostgreSQL scaffold pins pgx
160+
161+
- GIVEN a new project generated with `db_driver: PostgreSQL`
162+
- WHEN go.mod is generated
163+
- THEN it requires `github.com/jackc/pgx/v5`
164+
165+
#### Scenario: MySQL scaffold pins mysql driver
166+
167+
- GIVEN a new project generated with `db_driver: MySQL`
168+
- WHEN go.mod is generated
169+
- THEN it requires `github.com/go-sql-driver/mysql`
170+
171+
#### Scenario: No database has no driver dep
172+
173+
- GIVEN a new project generated with `db_driver: None`
174+
- WHEN go.mod is generated
175+
- THEN no DB driver dependency is present

0 commit comments

Comments
 (0)