From 12bd055d5021540f041849a87c83582484ca55fb Mon Sep 17 00:00:00 2001 From: Eduard Barrera <119897290+eduardbar@users.noreply.github.com> Date: Mon, 3 Aug 2026 12:34:18 -0500 Subject: [PATCH 1/3] fix(cli): enforce zero debt score thresholds --- src/cli.ts | 46 ++++++++++-------------- tests/min-score-gate.test.ts | 69 ++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 28 deletions(-) create mode 100644 tests/min-score-gate.test.ts diff --git a/src/cli.ts b/src/cli.ts index 81f70ff..cbe204d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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 ', '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 ', '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`) @@ -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) } }), @@ -901,8 +891,8 @@ addResourceOptions( .description('Emit GitHub Actions annotations and step summary') .option('--format ', 'Output format: console|json|markdown|ai|sarif') .option('--json', 'Output raw JSON report (legacy alias for --format json)') - .option('--min-score ', '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 ', '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)) @@ -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) } }), diff --git a/tests/min-score-gate.test.ts b/tests/min-score-gate.test.ts new file mode 100644 index 0000000..6e95f74 --- /dev/null +++ b/tests/min-score-gate.test.ts @@ -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) + }) +}) From 665b6b62ed4f3703b2f8e51b1b7c4d08f3df8753 Mon Sep 17 00:00:00 2001 From: Eduard Barrera <119897290+eduardbar@users.noreply.github.com> Date: Mon, 3 Aug 2026 15:35:29 -0500 Subject: [PATCH 2/3] refactor(debt): reduce actionable CLI and site findings --- site/src/App.jsx | 36 ++++-- site/src/components/HeaderHero.jsx | 147 +++++++++++-------------- src/guard.ts | 2 +- tests/phase1-init-doctor-guard.test.ts | 6 +- tests/site-header-hero.test.ts | 48 ++++++++ 5 files changed, 145 insertions(+), 94 deletions(-) create mode 100644 tests/site-header-hero.test.ts diff --git a/site/src/App.jsx b/site/src/App.jsx index ef99d60..f984ad6 100644 --- a/site/src/App.jsx +++ b/site/src/App.jsx @@ -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); @@ -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" } }); @@ -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", @@ -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" @@ -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", diff --git a/site/src/components/HeaderHero.jsx b/site/src/components/HeaderHero.jsx index 5289473..3bcdf52 100644 --- a/site/src/components/HeaderHero.jsx +++ b/site/src/components/HeaderHero.jsx @@ -1,87 +1,74 @@ +const NPM_PACKAGE_URL = "https://www.npmjs.com/package/@eduardbar/drift"; + +function HeroNavigation() { + return ( + + ); +} + +function HeroCopy() { + return ( +
+

Static trust audit for TS/JS repos

+

+ Merge with evidence, + not intuition. +

+

+ drift parses your repo with ts-morph, scores structural debt across 35 weighted + rules, and turns every PR into a measurable trust signal. +

+
    +
  • 35 weighted rules
  • +
  • AST-based analysis
  • +
  • CI ready trust gate
  • +
+ +

npm i -D @eduardbar/drift

+
+ ); +} + +function AuditSnapshot() { + return ( + + ); +} + export function HeaderHero() { return (
- - +
-
-

Static trust audit for TS/JS repos

-

- Merge with evidence, - not intuition. -

-

- drift parses your repo with ts-morph, scores structural debt across 35 weighted - rules, and turns every PR into a measurable trust signal. -

-
    -
  • 35 weighted rules
  • -
  • AST-based analysis
  • -
  • CI ready trust gate
  • -
- -

npm i -D @eduardbar/drift

-
- - + +
); diff --git a/src/guard.ts b/src/guard.ts index b8916a1..25d443e 100644 --- a/src/guard.ts +++ b/src/guard.ts @@ -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) } diff --git a/tests/phase1-init-doctor-guard.test.ts b/tests/phase1-init-doctor-guard.test.ts index 157b9fa..fa0ef2b 100644 --- a/tests/phase1-init-doctor-guard.test.ts +++ b/tests/phase1-init-doctor-guard.test.ts @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { runDoctor } from '../src/doctor.js' import { runInit } from '../src/init.js' -import { evaluateGuard, formatGuardJsonObject, runGuard } from '../src/guard.js' +import { evaluateGuard, formatGuardJson, runGuard } from '../src/guard.js' import { analyzeProject } from '../src/analyzer.js' import { buildReport } from '../src/reporter.js' @@ -271,7 +271,7 @@ describe('phase 1: doctor/init/guard', () => { budget: 0, bySeverity: { error: 0, warning: 0, info: 0 }, }) - const resultJson = formatGuardJsonObject(result) + const resultJson = JSON.parse(formatGuardJson(result)) as Record const schema = loadSchema('drift-guard.v1.json') const schemaErrors = validateAgainstSchema(schema, JSON.parse(JSON.stringify(resultJson))) @@ -284,7 +284,7 @@ describe('phase 1: doctor/init/guard', () => { expect(result.checks.some((check) => check.id === 'no-regression-total-issues')).toBe(true) expect(resultJson.$schema).toBe('schemas/drift-guard.v1.json') expect(typeof resultJson.toolVersion).toBe('string') - expect(resultJson.toolVersion.length).toBeGreaterThan(0) + expect(String(resultJson.toolVersion).length).toBeGreaterThan(0) expect(schemaErrors).toEqual([]) }, 30000) diff --git a/tests/site-header-hero.test.ts b/tests/site-header-hero.test.ts new file mode 100644 index 0000000..2f0bef1 --- /dev/null +++ b/tests/site-header-hero.test.ts @@ -0,0 +1,48 @@ +import { createElement } from "../site/node_modules/react/index.js"; +import { renderToStaticMarkup } from "../site/node_modules/react-dom/server.js"; +import { HeaderHero } from "../site/src/components/HeaderHero.jsx"; + +function renderHero() { + return renderToStaticMarkup(createElement(HeaderHero)); +} + +function renderedLinks(markup: string) { + return [...markup.matchAll(/]*)>([\s\S]*?)<\/a>/g)].map(([, attributes, content]) => ({ + attributes, + content: content.replace(/<[^>]+>/g, "").trim(), + })); +} + +function attribute(attributes: string, name: string) { + return attributes.match(new RegExp(`${name}="([^"]*)"`))?.[1]; +} + +describe("HeaderHero rendered contract", () => { + test("keeps the npm links and section navigation usable", () => { + const links = renderedLinks(renderHero()); + const npmLinks = links.filter( + ({ attributes }) => attribute(attributes, "href") === "https://www.npmjs.com/package/@eduardbar/drift" + ); + + expect(npmLinks).toHaveLength(2); + expect(npmLinks.every(({ attributes }) => attribute(attributes, "target") === "_blank")).toBe(true); + expect(npmLinks.every(({ attributes }) => attribute(attributes, "rel") === "noreferrer")).toBe(true); + expect(links.some(({ attributes, content }) => attribute(attributes, "href") === "#features" && content === "Features")).toBe(true); + expect(links.some(({ attributes, content }) => attribute(attributes, "href") === "#commands" && content === "Commands")).toBe(true); + }); + + test("keeps the visible proof points and audit metrics", () => { + const markup = renderHero(); + + expect(markup).toContain("35 weighted rules"); + expect(markup).toContain("AST-based analysis"); + expect(markup).toContain("CI ready trust gate"); + expect(markup).toContain("drift scan src"); + expect(markup).toContain("drift guard src --budget 3"); + expect(markup).toContain("drift trust src"); + expect(markup).toContain("drift trust-gate trust.json"); + expect(markup).toContain("Rule IDs: 35"); + expect(markup).toContain("Trust: 84"); + expect(markup).toContain("Delta risk: +2"); + }); +}); From 83454cdc66fdd6b61ca3bd24f94f53897747a700 Mon Sep 17 00:00:00 2001 From: Eduard Barrera <119897290+eduardbar@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:01:00 -0500 Subject: [PATCH 3/3] test(site): resolve SSR runtime per package layout --- tests/site-header-hero.test.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/site-header-hero.test.ts b/tests/site-header-hero.test.ts index 2f0bef1..b09aeab 100644 --- a/tests/site-header-hero.test.ts +++ b/tests/site-header-hero.test.ts @@ -1,7 +1,15 @@ -import { createElement } from "../site/node_modules/react/index.js"; -import { renderToStaticMarkup } from "../site/node_modules/react-dom/server.js"; +import { existsSync } from "node:fs"; +import { createRequire } from "node:module"; +import { resolve } from "node:path"; import { HeaderHero } from "../site/src/components/HeaderHero.jsx"; +const runtimePackageJson = existsSync(resolve(process.cwd(), "site/node_modules/react/package.json")) + ? resolve(process.cwd(), "site/package.json") + : resolve(process.cwd(), "package.json"); +const runtimeRequire = createRequire(runtimePackageJson); +const { createElement } = runtimeRequire("react") as typeof import("react"); +const { renderToStaticMarkup } = runtimeRequire("react-dom/server") as typeof import("react-dom/server"); + function renderHero() { return renderToStaticMarkup(createElement(HeaderHero)); }