feat: add schema drift detection to prevent false-positive verification

GSD agents silently skip database schema push — verification passes but
production breaks because TypeScript types come from config, not the live
database. This adds two layers of protection:

1. Plan-phase template detection (step 5.7): When the planner detects
   schema-relevant file patterns in the phase scope, it injects a mandatory
   [BLOCKING] schema push task into the plan with the appropriate push
   command for the detected ORM.

2. Post-execution drift detection gate: After execution completes but
   before verification marks success, scans for schema-relevant file
   changes and checks if a push command was executed. Blocks verification
   with actionable guidance if drift is detected.

Supports Payload CMS, Prisma, Drizzle, Supabase, and TypeORM.
Override with GSD_SKIP_SCHEMA_CHECK=true.

Fixes #1381

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-03-25 17:47:38 -04:00
parent 8502c11d97
commit 7f11543691
6 changed files with 809 additions and 2 deletions

View File

@@ -601,6 +601,72 @@ Options:
Use AskUserQuestion to present the options.
</step>
<step name="schema_drift_gate">
Post-execution schema drift detection. Catches false-positive verification where
build/types pass because TypeScript types come from config, not the live database.
**Run after execution completes but BEFORE verification marks success.**
```bash
SCHEMA_DRIFT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" verify schema-drift "${PHASE_NUMBER}" 2>/dev/null)
```
Parse JSON result for: `drift_detected`, `blocking`, `schema_files`, `orms`, `unpushed_orms`, `message`.
**If `drift_detected` is false:** Skip to verify_phase_goal.
**If `drift_detected` is true AND `blocking` is true:**
Check for override:
```bash
SKIP_SCHEMA=$(echo "${GSD_SKIP_SCHEMA_CHECK:-false}")
```
**If `SKIP_SCHEMA` is `true`:**
Display:
```
⚠ Schema drift detected but GSD_SKIP_SCHEMA_CHECK=true — bypassing gate.
Schema files changed: {schema_files}
ORMs requiring push: {unpushed_orms}
Proceeding to verification (database may be out of sync).
```
→ Continue to verify_phase_goal.
**If `SKIP_SCHEMA` is not `true`:**
BLOCK verification. Display:
```
## BLOCKED: Schema Drift Detected
Schema-relevant files changed during this phase but no database push command
was executed. Build and type checks pass because TypeScript types come from
config, not the live database — verification would produce a false positive.
Schema files changed: {schema_files}
ORMs requiring push: {unpushed_orms}
Required push commands:
{For each unpushed ORM, show the push command from the message}
Options:
1. Run push command now (recommended) — execute the push, then re-verify
2. Skip schema check (GSD_SKIP_SCHEMA_CHECK=true) — bypass this gate
3. Abort — stop execution and investigate
```
If `TEXT_MODE` is true, present as a plain-text numbered list. Otherwise use AskUserQuestion.
**If user selects option 1:** Present the specific push command(s) to run. After user confirms execution, re-run the schema drift check. If it passes, continue to verify_phase_goal.
**If user selects option 2:** Set override and continue to verify_phase_goal.
**If user selects option 3:** Stop execution. Report partial completion.
</step>
<step name="verify_phase_goal">
Verify phase achieved its GOAL, not just completed tasks.

View File

@@ -435,7 +435,69 @@ Otherwise use AskUserQuestion:
- "Continue without UI-SPEC" → Continue to step 6.
- "Not a frontend phase" → Continue to step 6.
**If `HAS_UI` is 1 (no frontend indicators):** Skip silently to step 6.
**If `HAS_UI` is 1 (no frontend indicators):** Skip silently to step 5.7.
## 5.7. Schema Push Detection Gate
> Detects schema-relevant files in the phase scope and injects a mandatory `[BLOCKING]` schema push task into the plan. Prevents false-positive verification where build/types pass because TypeScript types come from config, not the live database.
Check if any files in the phase scope match schema patterns:
```bash
PHASE_SECTION=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" roadmap get-phase "${PHASE}" --pick section 2>/dev/null)
```
Scan `PHASE_SECTION`, `CONTEXT.md` (if loaded), and `RESEARCH.md` (if exists) for file paths matching these ORM patterns:
| ORM | File Patterns |
|-----|--------------|
| Payload CMS | `src/collections/**/*.ts`, `src/globals/**/*.ts` |
| Prisma | `prisma/schema.prisma`, `prisma/schema/*.prisma` |
| Drizzle | `drizzle/schema.ts`, `src/db/schema.ts`, `drizzle/*.ts` |
| Supabase | `supabase/migrations/*.sql` |
| TypeORM | `src/entities/**/*.ts`, `src/migrations/**/*.ts` |
Also check if any existing PLAN.md files for this phase already reference these file patterns in `files_modified`.
**If schema-relevant files detected:**
Set `SCHEMA_PUSH_REQUIRED=true` and `SCHEMA_ORM={detected_orm}`.
Determine the push command for the detected ORM:
| ORM | Push Command | Non-TTY Workaround |
|-----|-------------|-------------------|
| Payload CMS | `npx payload migrate` | `CI=true PAYLOAD_MIGRATING=true npx payload migrate` |
| Prisma | `npx prisma db push` | `npx prisma db push --accept-data-loss` (if destructive) |
| Drizzle | `npx drizzle-kit push` | `npx drizzle-kit push` |
| Supabase | `supabase db push` | Set `SUPABASE_ACCESS_TOKEN` env var |
| TypeORM | `npx typeorm migration:run` | `npx typeorm migration:run -d src/data-source.ts` |
Inject the following into the planner prompt (step 8) as an additional constraint:
```markdown
<schema_push_requirement>
**[BLOCKING] Schema Push Required**
This phase modifies schema-relevant files ({detected_files}). The planner MUST include
a `[BLOCKING]` task that runs the database schema push command AFTER all schema file
modifications are complete but BEFORE verification.
- ORM detected: {SCHEMA_ORM}
- Push command: {push_command}
- Non-TTY workaround: {env_hint}
- If push requires interactive prompts that cannot be suppressed, flag the task for
manual intervention with `autonomous: false`
This task is mandatory — the phase CANNOT pass verification without it. Build and
type checks will pass without the push (types come from config, not the live database),
creating a false-positive verification state.
</schema_push_requirement>
```
Display: `Schema files detected ({SCHEMA_ORM}) — [BLOCKING] push task will be injected into plans`
**If no schema-relevant files detected:** Skip silently to step 6.
## 6. Check Existing Plans