KeiSeiKit-1.0/_primitives/_rust/kei-db-contract/src/main.rs
Parfii-bot f3f5f79760 feat(frontend-loop): kei-db-contract primitive + frontend-validator agent + auto-dev-guard hook
Frontend continuous-quality loop landed. Three composable cubes:

Wave 1 — kei-db-contract primitive (~870 LOC, 7 cubes per Constructor Pattern):
- Diffs SQL CREATE TABLE migrations against TypeScript type/interface declarations
- 4 drift modes: ORPHAN-SQL, ORPHAN-TS, TYPE-MISMATCH, NULL-MISMATCH
- Reuses sqlparser-rs (Apache 2.0) + regex + walkdir + serde_json + clap
- CLI: kei-db-contract <project-root> [--output json|text] [--strict]
- 5/5 integration tests pass (cargo check + cargo test green)
- Smoke-tested on keisei-marketplace: drift_count=266 across 30 tables
  (expected — marketplace uses raw better-sqlite3 without explicit row types)

Wave 2 — frontend-validator agent + dev-guard skill extension:
- New _manifests/frontend-validator.toml (substrate_role: edit-local, tools: Bash+Read+Glob+Grep)
- Agent runs: stack detect → tsc --noEmit → eslint → kei-db-contract → playwright (optional)
- Severity rules: TYPE_CHECK FAIL = block, DB_CONTRACT drift > 0 = block, lint = advisory
- skills/dev-guard/SKILL.md extended: 4th agent triggered on .tsx/.ts/.dart edits or DB-layer touches
- adaptive-depth table extended with frontend + DB-layer rows

Wave 3 — auto-dev-guard.sh hook (PostToolUse:Edit|Write):
- Trivial-edit gate: skip if delta < 30 LOC (avoid spawn fatigue)
- File-pattern match: *.tsx|*.ts|*.svelte|*.vue|*.dart OR migrations/*.sql OR src/db/** OR src/types/** OR prisma/schema.prisma OR drizzle.config.*
- Auto-runs kei-db-contract for DB-layer edits if binary on PATH
- Stderr advisory only (exit 0 always — never blocks)
- Bypass: KEI_DISABLED_HOOKS or KEI_HOOK_PROFILE in {advisory-off, minimal, off}
- Smoke-tested with synthetic Edit input (39 LOC delta on .tsx → emits advisory)
- Registered in hooks/hooks.json under PostToolUse:Write|Edit chain

Reusability map (Constructor Pattern compose):
  shared cubes: detect-stack, tsc, eslint, kei-db-contract, kei-visual-snapshot (deferred)
  orchestrators: /dev-start (pre), /dev-guard (during, NOW with frontend-validator),
                 /dev-ship (final), /site-create (init)

Verify-before-commit (RULE 0.13):
- cargo check -p kei-db-contract: PASS
- cargo test -p kei-db-contract: 5 passed
- jq . hooks/hooks.json: valid
- bash hooks/auto-dev-guard.sh < synthetic-input: works (frontend-relevant edit detected, exit 0)

=== STATUS-TRUTH MARKER ===
shipped: functional
stubs: 0
cargo-check: PASS
cargo-test: PASS (5 tests, 0 failures)
behaviour-verified: yes
follow-up-required:
  - kei-visual-snapshot primitive (Playwright wrap) — Wave 4, deferred
  - /dev-start frontend-contract-designer agent + /dev-ship frontend-final-gate — Wave 5, after Wave 1-3 obkatka
  - install.sh wiring for kei-db-contract binary
  - hermes-style emit-on-drift advisory mode

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 15:34:39 +08:00

68 lines
2 KiB
Rust

//! kei-db-contract — CLI entrypoint.
//!
//! Diffs SQL migrations against TypeScript types in a project root.
//! Exit 0 when --strict not set OR no drift; exit 1 in --strict with drift; exit 2 on I/O error.
use clap::{Parser, ValueEnum};
use kei_db_contract::diff::diff_project;
use kei_db_contract::output::{render_json, render_text};
use kei_db_contract::sql_parse::parse_migrations_dir;
use kei_db_contract::ts_parse::parse_ts_glob;
use std::path::PathBuf;
use std::process::ExitCode;
#[derive(Parser, Debug)]
#[command(
name = "kei-db-contract",
about = "Diff SQL migrations against TypeScript types to catch drift."
)]
struct Cli {
/// Project root.
project_root: PathBuf,
/// Migrations directory (relative to project root).
#[arg(long, default_value = "migrations")]
migrations_dir: PathBuf,
/// TS source root (relative to project root). Walked recursively for `.ts`/`.tsx`.
#[arg(long, default_value = "src")]
types_dir: PathBuf,
/// Output format.
#[arg(long, value_enum, default_value_t = Format::Text)]
output: Format,
/// Exit 1 if drift_count > 0.
#[arg(long)]
strict: bool,
}
#[derive(Clone, Copy, Debug, ValueEnum)]
enum Format {
Text,
Json,
}
fn main() -> ExitCode {
let cli = Cli::parse();
match run(cli) {
Ok(code) => code,
Err(err) => {
eprintln!("kei-db-contract: error: {:?}", err);
ExitCode::from(2)
}
}
}
fn run(cli: Cli) -> anyhow::Result<ExitCode> {
let mig_dir = cli.project_root.join(&cli.migrations_dir);
let ts_dir = cli.project_root.join(&cli.types_dir);
let tables = parse_migrations_dir(&mig_dir)?;
let ts_types = parse_ts_glob(&[ts_dir.as_path()])?;
let report = diff_project(&tables, &ts_types);
let text = match cli.output {
Format::Text => render_text(&report),
Format::Json => render_json(&report),
};
println!("{}", text);
if cli.strict && report.drift_count > 0 {
return Ok(ExitCode::from(1));
}
Ok(ExitCode::SUCCESS)
}