Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 26 additions & 10 deletions site/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@ import { SiteFooter } from "./components/SiteFooter";

gsap.registerPlugin(ScrollTrigger, useGSAP);

const FLOW_GLOW_INITIAL_Y = -10;
const REVEAL_LEFT_OFFSET = -34;
const FLOW_INACTIVE_OPACITY = 0.62;
const FLOW_ACTIVE_Y = -4;
const FLOW_ACTIVE_SCALE = 1.02;
const FLOW_INACTIVE_SCALE = 0.985;
const FLOW_ACTIVE_DURATION = 0.4;
const FLOW_INACTIVE_DURATION = 0.34;
const FLOW_GLOW_BASE_OPACITY = 0.42;
const FLOW_GLOW_OPACITY_STEP = 0.18;
const FLOW_GLOW_BASE_Y = -12;
const FLOW_GLOW_Y_STEP = 8;
const FLOW_GLOW_BASE_SCALE = 0.96;
const FLOW_GLOW_SCALE_STEP = 0.06;
const HERO_PARALLAX_PERCENT = -8;

export default function App() {
const rootRef = useRef(null);

Expand Down Expand Up @@ -74,7 +90,7 @@ export default function App() {
});
gsap.set(".js-divider-line", { scaleX: 0, transformOrigin: "0% 50%" });
gsap.set(".js-flow-meter", { scaleY: 0, transformOrigin: "50% 0%" });
gsap.set(".js-flow-glow", { autoAlpha: 0.4, scale: 0.92, y: -10 });
gsap.set(".js-flow-glow", { autoAlpha: 0.4, scale: 0.92, y: FLOW_GLOW_INITIAL_Y });

const heroTimeline = gsap.timeline({ defaults: { ease: "power3.out" } });

Expand Down Expand Up @@ -122,7 +138,7 @@ export default function App() {
});

gsap.from(".js-reveal-left", {
x: -34,
x: REVEAL_LEFT_OFFSET,
autoAlpha: 0,
duration: 0.7,
ease: "power2.out",
Expand Down Expand Up @@ -173,19 +189,19 @@ export default function App() {
stage.classList.toggle("is-active", isActive);

gsap.to(stage, {
autoAlpha: isActive ? 1 : 0.62,
y: isActive ? -4 : 0,
scale: isActive ? 1.02 : 0.985,
duration: isActive ? 0.4 : 0.34,
autoAlpha: isActive ? 1 : FLOW_INACTIVE_OPACITY,
y: isActive ? FLOW_ACTIVE_Y : 0,
scale: isActive ? FLOW_ACTIVE_SCALE : FLOW_INACTIVE_SCALE,
duration: isActive ? FLOW_ACTIVE_DURATION : FLOW_INACTIVE_DURATION,
ease: isActive ? "power3.out" : "power2.out",
overwrite: "auto"
});
});

gsap.to(".js-flow-glow", {
autoAlpha: 0.42 + activeIndex * 0.18,
y: -12 + activeIndex * 8,
scale: 0.96 + activeIndex * 0.06,
autoAlpha: FLOW_GLOW_BASE_OPACITY + activeIndex * FLOW_GLOW_OPACITY_STEP,
y: FLOW_GLOW_BASE_Y + activeIndex * FLOW_GLOW_Y_STEP,
scale: FLOW_GLOW_BASE_SCALE + activeIndex * FLOW_GLOW_SCALE_STEP,
duration: 0.45,
ease: "power2.out",
overwrite: "auto"
Expand Down Expand Up @@ -270,7 +286,7 @@ export default function App() {

if (desktop) {
gsap.to(".js-hero-panel", {
yPercent: -8,
yPercent: HERO_PARALLAX_PERCENT,
ease: "none",
scrollTrigger: {
trigger: ".hero",
Expand Down
147 changes: 67 additions & 80 deletions site/src/components/HeaderHero.jsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,74 @@
const NPM_PACKAGE_URL = "https://www.npmjs.com/package/@eduardbar/drift";

function HeroNavigation() {
return (
<nav className="top-nav js-hero-nav" aria-label="Primary">
<p className="brand">@eduardbar/drift</p>
<div className="nav-links" role="list" aria-label="Key sections">
<a className="chip-link" href="#features" role="listitem">Features</a>
<a className="chip-link" href="#commands" role="listitem">Commands</a>
<a className="chip-link" href={NPM_PACKAGE_URL} target="_blank" rel="noreferrer" role="listitem">
npm
</a>
</div>
</nav>
);
}

function HeroCopy() {
return (
<div className="hero-copy">
<p className="eyebrow js-hero-kicker">Static trust audit for TS/JS repos</p>
<h1>
<span className="hero-title-line js-hero-title-line">Merge with evidence,</span>
<span className="hero-title-line js-hero-title-line">not intuition.</span>
</h1>
<p className="hero-subtext js-hero-sub">
drift parses your repo with ts-morph, scores structural debt across 35 weighted
rules, and turns every PR into a measurable trust signal.
</p>
<ul className="hero-proof js-hero-proof" aria-label="Quick proof points">
<li>35 weighted rules</li>
<li>AST-based analysis</li>
<li>CI ready trust gate</li>
</ul>
<div className="hero-actions js-hero-actions">
<a className="btn btn-primary" href={NPM_PACKAGE_URL} target="_blank" rel="noreferrer">
Install drift
</a>
<a className="btn btn-secondary" href="#commands">Review command flow</a>
</div>
<p className="hero-footnote js-hero-footnote">npm i -D @eduardbar/drift</p>
</div>
);
}

function AuditSnapshot() {
return (
<aside className="hero-panel js-hero-panel" aria-label="drift command preview">
<p className="panel-title">Audit snapshot</p>
<div className="terminal-lines">
<p className="js-terminal-line"><span>$</span> drift scan src</p>
<p className="js-terminal-line"><span>$</span> drift guard src --budget 3</p>
<p className="js-terminal-line"><span>$</span> drift trust src</p>
<p className="js-terminal-line"><span>$</span> drift trust-gate trust.json</p>
</div>
<div className="kpi-row" role="list" aria-label="Sample quality metrics">
<p role="listitem" className="js-hero-kpi"><strong>Rule IDs:</strong> 35</p>
<p role="listitem" className="js-hero-kpi"><strong>Trust:</strong> 84</p>
<p role="listitem" className="js-hero-kpi"><strong>Delta risk:</strong> +2</p>
</div>
</aside>
);
}

export function HeaderHero() {
return (
<header className="hero shell" id="top">
<nav className="top-nav js-hero-nav" aria-label="Primary">
<p className="brand">@eduardbar/drift</p>
<div className="nav-links" role="list" aria-label="Key sections">
<a className="chip-link" href="#features" role="listitem">
Features
</a>
<a className="chip-link" href="#commands" role="listitem">
Commands
</a>
<a
className="chip-link"
href="https://www.npmjs.com/package/@eduardbar/drift"
target="_blank"
rel="noreferrer"
role="listitem"
>
npm
</a>
</div>
</nav>

<HeroNavigation />
<div className="hero-grid">
<div className="hero-copy">
<p className="eyebrow js-hero-kicker">Static trust audit for TS/JS repos</p>
<h1>
<span className="hero-title-line js-hero-title-line">Merge with evidence,</span>
<span className="hero-title-line js-hero-title-line">not intuition.</span>
</h1>
<p className="hero-subtext js-hero-sub">
drift parses your repo with ts-morph, scores structural debt across 35 weighted
rules, and turns every PR into a measurable trust signal.
</p>
<ul className="hero-proof js-hero-proof" aria-label="Quick proof points">
<li>35 weighted rules</li>
<li>AST-based analysis</li>
<li>CI ready trust gate</li>
</ul>
<div className="hero-actions js-hero-actions">
<a
className="btn btn-primary"
href="https://www.npmjs.com/package/@eduardbar/drift"
target="_blank"
rel="noreferrer"
>
Install drift
</a>
<a className="btn btn-secondary" href="#commands">
Review command flow
</a>
</div>
<p className="hero-footnote js-hero-footnote">npm i -D @eduardbar/drift</p>
</div>

<aside className="hero-panel js-hero-panel" aria-label="drift command preview">
<p className="panel-title">Audit snapshot</p>
<div className="terminal-lines">
<p className="js-terminal-line">
<span>$</span> drift scan src
</p>
<p className="js-terminal-line">
<span>$</span> drift guard src --budget 3
</p>
<p className="js-terminal-line">
<span>$</span> drift trust src
</p>
<p className="js-terminal-line">
<span>$</span> drift trust-gate trust.json
</p>
</div>
<div className="kpi-row" role="list" aria-label="Sample quality metrics">
<p role="listitem" className="js-hero-kpi">
<strong>Rule IDs:</strong> 35
</p>
<p role="listitem" className="js-hero-kpi">
<strong>Trust:</strong> 84
</p>
<p role="listitem" className="js-hero-kpi">
<strong>Delta risk:</strong> +2
</p>
</div>
</aside>
<HeroCopy />
<AuditSnapshot />
</div>
</header>
);
Expand Down
46 changes: 18 additions & 28 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,8 +162,8 @@ addResourceOptions(
.option('--json', 'Output raw JSON report')
.option('--ai', 'Output AI-optimized JSON for LLM consumption')
.option('--fix', 'Show fix suggestions for each issue')
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
.action(async (targetPath: string | undefined, options: { output?: string; format?: string; json?: boolean; ai?: boolean; fix?: boolean; minScore: string } & ResourceOptionFlags) => {
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold')
.action(async (targetPath: string | undefined, options: { output?: string; format?: string; json?: boolean; ai?: boolean; fix?: boolean; minScore?: string } & ResourceOptionFlags) => {
const resolvedPath = resolve(targetPath ?? '.')

process.stderr.write(`\nScanning ${resolvedPath}...\n`)
Expand All @@ -185,37 +185,27 @@ addResourceOptions(

if (format === 'sarif') {
process.stdout.write(`${JSON.stringify(toSarif(report), null, 2)}\n`)
return
}

if (format === 'ai') {
} else if (format === 'ai') {
const aiOutput = formatAIOutput(report)
process.stdout.write(JSON.stringify(aiOutput, null, 2))
return
}

if (format === 'json') {
} else if (format === 'json') {
process.stdout.write(JSON.stringify(report, null, 2))
return
}

if (format === 'markdown') {
} else if (format === 'markdown') {
process.stdout.write(`${formatMarkdown(report)}\n`)
return
}

printConsole(report, { showFix: options.fix })
} else {
printConsole(report, { showFix: options.fix })

if (options.output) {
const md = formatMarkdown(report)
const outPath = resolve(options.output)
writeFileSync(outPath, md, 'utf8')
// drift-ignore
console.error(`Report saved to ${outPath}`)
if (options.output) {
const md = formatMarkdown(report)
const outPath = resolve(options.output)
writeFileSync(outPath, md, 'utf8')
// drift-ignore
console.error(`Report saved to ${outPath}`)
}
}

const minScore = Number(options.minScore)
if (minScore > 0 && report.totalScore > minScore) {
if (options.minScore !== undefined && report.totalScore > minScore) {
process.exit(1)
}
}),
Expand Down Expand Up @@ -901,8 +891,8 @@ addResourceOptions(
.description('Emit GitHub Actions annotations and step summary')
.option('--format <type>', 'Output format: console|json|markdown|ai|sarif')
.option('--json', 'Output raw JSON report (legacy alias for --format json)')
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
.action(async (targetPath: string | undefined, options: { format?: string; json?: boolean; minScore: string } & ResourceOptionFlags) => {
.option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold')
.action(async (targetPath: string | undefined, options: { format?: string; json?: boolean; minScore?: string } & ResourceOptionFlags) => {
const resolvedPath = resolve(targetPath ?? '.')
const config = await loadConfig(resolvedPath)
const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options))
Expand All @@ -925,7 +915,7 @@ addResourceOptions(
printCISummary(report)
}
const minScore = Number(options.minScore)
if (minScore > 0 && report.totalScore > minScore) {
if (options.minScore !== undefined && report.totalScore > minScore) {
process.exit(1)
}
}),
Expand Down
2 changes: 1 addition & 1 deletion src/guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export function evaluateGuard(input: GuardEvalInput): GuardEvaluation {
}
}

export function formatGuardJsonObject(result: GuardResult): GuardResultJson {
function formatGuardJsonObject(result: GuardResult): GuardResultJson {
return withOutputMetadata(result, OUTPUT_SCHEMA.guard)
}

Expand Down
69 changes: 69 additions & 0 deletions tests/min-score-gate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { afterEach, describe, expect, it } from 'vitest'
import { spawnSync } from 'node:child_process'
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'

type CliResult = {
status: number | null
stdout: string
stderr: string
}

function runCli(args: string[]): CliResult {
const result = spawnSync(process.execPath, ['--import', 'tsx', 'src/cli.ts', ...args], {
cwd: process.cwd(),
encoding: 'utf8',
})

return {
status: result.status,
stdout: result.stdout,
stderr: result.stderr,
}
}

describe('min-score gate', () => {
let tmpDir = ''

afterEach(() => {
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true })
tmpDir = ''
})

it('fails scan --min-score 0 while preserving JSON output', () => {
tmpDir = mkdtempSync(join(tmpdir(), 'drift-min-score-scan-'))
writeFileSync(join(tmpDir, 'sample.ts'), 'console.log("debug")\n')

const result = runCli(['scan', tmpDir, '--format', 'json', '--min-score', '0'])

expect(result.status).toBe(1)
expect(JSON.parse(result.stdout).totalScore).toBeGreaterThan(0)
})

it('fails ci --min-score 0 while preserving human CI output', () => {
tmpDir = mkdtempSync(join(tmpdir(), 'drift-min-score-ci-'))
writeFileSync(join(tmpDir, 'sample.ts'), 'console.log("debug")\n')

const result = runCli(['ci', tmpDir, '--min-score', '0'])

expect(result.status).toBe(1)
expect(result.stdout).toContain('::warning')
})

it('keeps omitted thresholds unchanged and preserves strict positive thresholds', () => {
tmpDir = mkdtempSync(join(tmpdir(), 'drift-min-score-positive-'))
writeFileSync(join(tmpDir, 'sample.ts'), 'console.log("debug")\n')

const baseline = runCli(['scan', tmpDir, '--format', 'json'])
const score = JSON.parse(baseline.stdout).totalScore as number
const exact = runCli(['scan', tmpDir, '--format', 'json', '--min-score', String(score)])
const below = runCli(['scan', tmpDir, '--format', 'json', '--min-score', String(Math.max(0, score - 1))])

expect(baseline.status).toBe(0)
expect(exact.status).toBe(0)
expect(below.status).toBe(1)
expect(JSON.parse(exact.stdout).totalScore).toBe(score)
expect(JSON.parse(below.stdout).totalScore).toBe(score)
})
})
Loading
Loading